From 72ca4a4e39a1d4de03d6423480aa2ee82b021060 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 10 Nov 2021 08:30:39 -0800 Subject: [PATCH] fix: command completion The fake npm object in the tests wasn't returning an async function Fixes: https://github.com/npm/cli/issues/4020 PR-URL: https://github.com/npm/cli/pull/4032 Credit: @wraithgar Close: #4032 Reviewed-by: @lukekarrys --- lib/commands/completion.js | 15 +- .../test/lib/commands/completion.js.test.cjs | 239 ++++++ test/lib/commands/completion.js | 737 +++++------------- 3 files changed, 452 insertions(+), 539 deletions(-) create mode 100644 tap-snapshots/test/lib/commands/completion.js.test.cjs diff --git a/lib/commands/completion.js b/lib/commands/completion.js index bce6c3619ccc4..504528c906ec5 100644 --- a/lib/commands/completion.js +++ b/lib/commands/completion.js @@ -166,7 +166,7 @@ class Completion extends BaseCommand { // at this point, if words[1] is some kind of npm command, // then complete on it. // otherwise, do nothing - const impl = this.npm.cmd(cmd) + const impl = await this.npm.cmd(cmd) if (impl.completion) { const comps = await impl.completion(opts) return this.wrap(opts, comps) @@ -180,12 +180,10 @@ class Completion extends BaseCommand { // Ie, returning ['a', 'b c', ['d', 'e']] would allow it to expand // to: 'a', 'b c', or 'd' 'e' wrap (opts, compls) { - if (!Array.isArray(compls)) { - compls = compls ? [compls] : [] - } - - compls = compls.map(c => - Array.isArray(c) ? c.map(escape).join(' ') : escape(c)) + // TODO this was dead code, leaving it in case we find some command we + // forgot that requires this. if so *that command should fix its + // completions* + // compls = compls.map(w => !/\s+/.test(w) ? w : '\'' + w + '\'') if (opts.partialWord) { compls = compls.filter(c => c.startsWith(opts.partialWord)) @@ -246,9 +244,6 @@ const dumpScript = async () => { const unescape = w => w.charAt(0) === '\'' ? w.replace(/^'|'$/g, '') : w.replace(/\\ /g, ' ') -const escape = w => !/\s+/.test(w) ? w - : '\'' + w + '\'' - // the current word has a dash. Return the config names, // with the same number of dashes as the current word has. const configCompl = opts => { diff --git a/tap-snapshots/test/lib/commands/completion.js.test.cjs b/tap-snapshots/test/lib/commands/completion.js.test.cjs new file mode 100644 index 0000000000000..13a3f66ef7d15 --- /dev/null +++ b/tap-snapshots/test/lib/commands/completion.js.test.cjs @@ -0,0 +1,239 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/lib/commands/completion.js TAP completion --no- flags > flags 1`] = ` +Array [ + Array [ + String( + --no-version + --no-versions + ), + ], +] +` + +exports[`test/lib/commands/completion.js TAP completion commands with no completion > no results 1`] = ` +Array [] +` + +exports[`test/lib/commands/completion.js TAP completion completion cannot complete options that take a value in mid-command > does not try to complete option arguments in the middle of a command 1`] = ` +Array [] +` + +exports[`test/lib/commands/completion.js TAP completion completion completion > both shells 1`] = ` +Array [] +` + +exports[`test/lib/commands/completion.js TAP completion completion completion no known shells > no responses 1`] = ` +Array [] +` + +exports[`test/lib/commands/completion.js TAP completion completion completion wrong word count > no responses 1`] = ` +Array [] +` + +exports[`test/lib/commands/completion.js TAP completion completion of invalid command name does nothing > no results 1`] = ` +Array [] +` + +exports[`test/lib/commands/completion.js TAP completion double dashes escape from flag completion > full command list 1`] = ` +Array [ + Array [ + String( + ci + install-ci-test + install + install-test + uninstall + cache + config + set + get + update + outdated + prune + pack + find-dupes + dedupe + hook + rebuild + link + publish + star + stars + unstar + adduser + login + logout + unpublish + owner + access + team + deprecate + shrinkwrap + token + profile + audit + fund + org + help + ls + ll + search + view + init + version + edit + explore + docs + repo + bugs + root + prefix + bin + whoami + diff + dist-tag + ping + pkg + test + stop + start + restart + run-script + set-script + completion + doctor + exec + explain + un + rb + list + ln + create + i + it + cit + up + c + s + se + tst + t + ddp + v + run + clean-install + clean-install-test + x + why + la + verison + ic + innit + in + ins + inst + insta + instal + isnt + isnta + isntal + install-clean + isntall-clean + hlep + dist-tags + upgrade + udpate + login + add-user + author + home + issues + info + show + find + add + unlink + remove + rm + r + rum + sit + urn + ogr + ), + ], +] +` + +exports[`test/lib/commands/completion.js TAP completion filtered subcommands > filtered subcommands 1`] = ` +Array [ + Array [ + "public", + ], +] +` + +exports[`test/lib/commands/completion.js TAP completion flags > flags 1`] = ` +Array [ + Array [ + String( + --version + --versions + --viewer + --verbose + --v + ), + ], +] +` + +exports[`test/lib/commands/completion.js TAP completion multiple command names > multiple command names 1`] = ` +Array [ + Array [ + String( + adduser + access + audit + add-user + author + add + ), + ], +] +` + +exports[`test/lib/commands/completion.js TAP completion single command name > single command name 1`] = ` +Array [ + Array [ + "config", + ], +] +` + +exports[`test/lib/commands/completion.js TAP completion subcommand completion > subcommands 1`] = ` +Array [ + Array [ + String( + public + restricted + grant + revoke + ls-packages + ls-collaborators + edit + 2fa-required + 2fa-not-required + ), + ], +] +` + +exports[`test/lib/commands/completion.js TAP windows without bash > no output 1`] = ` +Array [] +` diff --git a/test/lib/commands/completion.js b/test/lib/commands/completion.js index 7a7e0a759fbfe..51212f06d888e 100644 --- a/test/lib/commands/completion.js +++ b/test/lib/commands/completion.js @@ -6,590 +6,269 @@ const completionScript = fs .readFileSync(path.resolve(__dirname, '../../../lib/utils/completion.sh'), { encoding: 'utf8' }) .replace(/^#!.*?\n/, '') -const output = [] -const npmConfig = {} -let accessCompletionError = false - -const npm = { - config: { - set: (key, value) => { - npmConfig[key] = value - }, - clear: () => { - for (const key in npmConfig) { - delete npmConfig[key] - } - }, - }, - cmd: cmd => { - return { - completion: { - completion: () => [['>>', '~/.bashrc']], - }, - adduser: {}, - access: { - completion: () => { - if (accessCompletionError) { - throw new Error('access completion failed') - } - - return ['public', 'restricted'] - }, - }, - promise: { - completion: () => Promise.resolve(['resolved_completion_promise']), - }, - donothing: { - completion: () => { - return null - }, - }, - driveaboat: { - completion: () => { - return ' fast' - }, - }, - }[cmd] - }, - output: line => { - output.push(line) - }, -} - -const cmdList = { - aliases: { - login: 'adduser', - }, - cmdList: ['access', 'adduser', 'completion'], - plumbing: [], -} - -// only include a subset so that the snapshots aren't huge and -// don't change when we add/remove config definitions. -const definitions = require('../../../lib/utils/config/definitions.js') -const config = { - definitions: { - global: definitions.global, - browser: definitions.browser, - registry: definitions.registry, - }, - shorthands: { - reg: ['--registry'], - }, -} - -const deref = cmd => { - return cmd -} - -const Completion = t.mock('../../../lib/commands/completion.js', { - '../../../lib/utils/cmd-list.js': cmdList, - '../../../lib/utils/config/index.js': config, - '../../../lib/utils/deref-command.js': deref, - '../../../lib/utils/is-windows-shell.js': false, -}) -const completion = new Completion(npm) - -t.test('completion completion', async t => { - const home = process.env.HOME - t.teardown(() => { - process.env.HOME = home - }) - - process.env.HOME = t.testdir({ - '.bashrc': '', - '.zshrc': '', - }) +const { real: mockNpm } = require('../../fixtures/mock-npm') - const res = await completion.completion({ w: 2 }) - t.strictSame( - res, - [ - ['>>', '~/.zshrc'], - ['>>', '~/.bashrc'], - ], - 'identifies both shells' - ) - t.end() +const { Npm, outputs } = mockNpm(t, { + '../../lib/utils/is-windows-shell.js': false, }) +const npm = new Npm() + +t.test('completion', async t => { + const completion = await npm.cmd('completion') + t.test('completion completion', async t => { + const home = process.env.HOME + t.teardown(() => { + process.env.HOME = home + }) -t.test('completion completion no known shells', async t => { - const home = process.env.HOME - t.teardown(() => { - process.env.HOME = home - }) + process.env.HOME = t.testdir({ + '.bashrc': '', + '.zshrc': '', + }) - process.env.HOME = t.testdir() + await completion.completion({ w: 2 }) + t.matchSnapshot(outputs, 'both shells') + }) - const res = await completion.completion({ w: 2 }) - t.strictSame(res, [], 'no responses') - t.end() -}) + t.test('completion completion no known shells', async t => { + const home = process.env.HOME + t.teardown(() => { + process.env.HOME = home + }) -t.test('completion completion wrong word count', async t => { - const res = await completion.completion({ w: 3 }) - t.strictSame(res, undefined, 'no responses') - t.end() -}) + process.env.HOME = t.testdir() -t.test('completion errors in windows without bash', async t => { - const Compl = t.mock('../../../lib/commands/completion.js', { - '../../../lib/utils/is-windows-shell.js': true, + await completion.completion({ w: 2 }) + t.matchSnapshot(outputs, 'no responses') }) - const compl = new Compl() - - await t.rejects( - compl.exec({}), - { code: 'ENOTSUP', message: /completion supported only in MINGW/ }, - 'returns the correct error' - ) -}) - -t.test('dump script when completion is not being attempted', async t => { - const _write = process.stdout.write - const _on = process.stdout.on - t.teardown(() => { - process.stdout.write = _write - process.stdout.on = _on + t.test('completion completion wrong word count', async t => { + await completion.completion({ w: 3 }) + t.matchSnapshot(outputs, 'no responses') }) - let errorHandler - process.stdout.on = (event, handler) => { - errorHandler = handler - process.stdout.on = _on - } - - let data - process.stdout.write = (chunk, callback) => { - data = chunk - process.stdout.write = _write - process.nextTick(() => { - callback() - errorHandler({ errno: 'EPIPE' }) + t.test('dump script when completion is not being attempted', async t => { + const _write = process.stdout.write + const _on = process.stdout.on + t.teardown(() => { + process.stdout.write = _write + process.stdout.on = _on }) - } - await completion.exec({}) + let errorHandler + process.stdout.on = (event, handler) => { + errorHandler = handler + process.stdout.on = _on + } + + let data + process.stdout.write = (chunk, callback) => { + data = chunk + process.stdout.write = _write + process.nextTick(() => { + callback() + errorHandler({ errno: 'EPIPE' }) + }) + } + + await completion.exec({}) + + t.equal(data, completionScript, 'wrote the completion script') + }) - t.equal(data, completionScript, 'wrote the completion script') -}) + t.test('dump script exits correctly when EPIPE is emitted on stdout', async t => { + const _write = process.stdout.write + const _on = process.stdout.on + t.teardown(() => { + process.stdout.write = _write + process.stdout.on = _on + }) -t.test('dump script exits correctly when EPIPE is emitted on stdout', async t => { - const _write = process.stdout.write - const _on = process.stdout.on - t.teardown(() => { - process.stdout.write = _write - process.stdout.on = _on + let errorHandler + process.stdout.on = (event, handler) => { + errorHandler = handler + process.stdout.on = _on + } + + let data + process.stdout.write = (chunk, callback) => { + data = chunk + process.stdout.write = _write + process.nextTick(() => { + errorHandler({ errno: 'EPIPE' }) + callback() + }) + } + + await completion.exec({}) + t.equal(data, completionScript, 'wrote the completion script') }) - let errorHandler - process.stdout.on = (event, handler) => { - errorHandler = handler - process.stdout.on = _on - } - - let data - process.stdout.write = (chunk, callback) => { - data = chunk - process.stdout.write = _write - process.nextTick(() => { - errorHandler({ errno: 'EPIPE' }) - callback() - }) - } + t.test('single command name', async t => { + process.env.COMP_CWORD = 1 + process.env.COMP_LINE = 'npm conf' + process.env.COMP_POINT = process.env.COMP_LINE.length - await completion.exec({}) - t.equal(data, completionScript, 'wrote the completion script') -}) + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) -// This test was only working by coincidence before, when we switch to full -// async/await the race condition now makes it impossible to test. The root of -// the problem is that if we override stdout.write then other things interfere -// during testing. -// t.test('non EPIPE errors cause failures', async t => { -// const _write = process.stdout.write -// const _on = process.stdout.on -// t.teardown(() => { -// process.stdout.write = _write -// process.stdout.on = _on -// }) - -// let errorHandler -// process.stdout.on = (event, handler) => { -// errorHandler = handler -// process.stdout.on = _on -// } - -// let data -// process.stdout.write = (chunk, callback) => { -// data = chunk -// process.stdout.write = _write -// process.nextTick(() => { -// errorHandler({ errno: 'ESOMETHINGELSE' }) -// callback() -// }) -// } - -// await t.rejects( -// completion.exec([]), -// { errno: 'ESOMETHINGELSE' }, -// 'propagated error' -// ) -// t.equal(data, completionScript, 'wrote the completion script') -// }) - -t.test('completion completes single command name', async t => { - process.env.COMP_CWORD = 1 - process.env.COMP_LINE = 'npm c' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + await completion.exec(['npm', 'conf']) + t.matchSnapshot(outputs, 'single command name') }) - await completion.exec(['npm', 'c']) - t.strictSame(output, ['completion'], 'correctly completed a command name') -}) + t.test('multiple command names', async t => { + process.env.COMP_CWORD = 1 + process.env.COMP_LINE = 'npm a' + process.env.COMP_POINT = process.env.COMP_LINE.length + + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) -t.test('completion completes command names', async t => { - process.env.COMP_CWORD = 1 - process.env.COMP_LINE = 'npm a' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + await completion.exec(['npm', 'a']) + t.matchSnapshot(outputs, 'multiple command names') }) - await completion.exec(['npm', 'a']) - t.strictSame(output, [['access', 'adduser'].join('\n')], 'correctly completed a command name') -}) + t.test('completion of invalid command name does nothing', async t => { + process.env.COMP_CWORD = 1 + process.env.COMP_LINE = 'npm compute' + process.env.COMP_POINT = process.env.COMP_LINE.length + + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) -t.test('completion of invalid command name does nothing', async t => { - process.env.COMP_CWORD = 1 - process.env.COMP_LINE = 'npm compute' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + await completion.exec(['npm', 'compute']) + t.matchSnapshot(outputs, 'no results') }) - await completion.exec(['npm', 'compute']) - t.strictSame(output, [], 'returns no results') -}) + t.test('subcommand completion', async t => { + process.env.COMP_CWORD = 2 + process.env.COMP_LINE = 'npm access ' + process.env.COMP_POINT = process.env.COMP_LINE.length -t.test('handles async completion function', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm promise' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) + + await completion.exec(['npm', 'access', '']) + t.matchSnapshot(outputs, 'subcommands') }) - await completion.exec(['npm', 'promise', '']) - - t.strictSame( - npmConfig, - { - argv: { - remain: ['npm', 'promise'], - cooked: ['npm', 'promise'], - original: ['npm', 'promise'], - }, - }, - 'applies command config appropriately' - ) - t.strictSame(output, ['resolved_completion_promise'], 'resolves async completion results') -}) + t.test('filtered subcommands', async t => { + process.env.COMP_CWORD = 2 + process.env.COMP_LINE = 'npm access p' + process.env.COMP_POINT = process.env.COMP_LINE.length -t.test('completion triggers command completions', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm access ' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) + + await completion.exec(['npm', 'access', 'p']) + t.matchSnapshot(outputs, 'filtered subcommands') }) - await completion.exec(['npm', 'access', '']) - - t.strictSame( - npmConfig, - { - argv: { - remain: ['npm', 'access'], - cooked: ['npm', 'access'], - original: ['npm', 'access'], - }, - }, - 'applies command config appropriately' - ) - t.strictSame( - output, - [['public', 'restricted'].join('\n')], - 'correctly completed a subcommand name' - ) -}) + t.test('commands with no completion', async t => { + process.env.COMP_CWORD = 2 + process.env.COMP_LINE = 'npm adduser ' + process.env.COMP_POINT = process.env.COMP_LINE.length + + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) -t.test('completion triggers filtered command completions', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm access p' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + // quotes around adduser are to ensure coverage when unescaping commands + await completion.exec(['npm', "'adduser'", '']) + t.matchSnapshot(outputs, 'no results') }) - await completion.exec(['npm', 'access', 'p']) - - t.strictSame( - npmConfig, - { - argv: { - remain: ['npm', 'access'], - cooked: ['npm', 'access'], - original: ['npm', 'access'], - }, - }, - 'applies command config appropriately' - ) - t.strictSame(output, ['public'], 'correctly completed a subcommand name') -}) + t.test('flags', async t => { + process.env.COMP_CWORD = 2 + process.env.COMP_LINE = 'npm install --v' + process.env.COMP_POINT = process.env.COMP_LINE.length -t.test('completions for commands that return nested arrays are joined', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm completion ' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 - }) + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) - await completion.exec(['npm', 'completion', '']) - - t.strictSame( - npmConfig, - { - argv: { - remain: ['npm', 'completion'], - cooked: ['npm', 'completion'], - original: ['npm', 'completion'], - }, - }, - 'applies command config appropriately' - ) - t.strictSame(output, ['>> ~/.bashrc'], 'joins nested arrays') -}) + await completion.exec(['npm', 'install', '--v']) -t.test('completions for commands that return nothing work correctly', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm donothing ' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + t.matchSnapshot(outputs, 'flags') }) - await completion.exec(['npm', 'donothing', '']) - - t.strictSame( - npmConfig, - { - argv: { - remain: ['npm', 'donothing'], - cooked: ['npm', 'donothing'], - original: ['npm', 'donothing'], - }, - }, - 'applies command config appropriately' - ) - t.strictSame(output, [], 'returns nothing') -}) + t.test('--no- flags', async t => { + process.env.COMP_CWORD = 2 + process.env.COMP_LINE = 'npm install --no-v' + process.env.COMP_POINT = process.env.COMP_LINE.length -t.test('completions for commands that return a single item work correctly', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm driveaboat ' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 - }) + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) - await completion.exec(['npm', 'driveaboat', '']) - t.strictSame( - npmConfig, - { - argv: { - remain: ['npm', 'driveaboat'], - cooked: ['npm', 'driveaboat'], - original: ['npm', 'driveaboat'], - }, - }, - 'applies command config appropriately' - ) - t.strictSame(output, ["' fast'"], 'returns the correctly escaped string') -}) + await completion.exec(['npm', 'install', '--no-v']) -t.test('command completion for commands with no completion return no results', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm adduser ' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + t.matchSnapshot(outputs, 'flags') }) - // quotes around adduser are to ensure coverage when unescaping commands - await completion.exec(['npm', "'adduser'", '']) - t.strictSame( - npmConfig, - { - argv: { - remain: ['npm', 'adduser'], - cooked: ['npm', 'adduser'], - original: ['npm', 'adduser'], - }, - }, - 'applies command config appropriately' - ) - t.strictSame(output, [], 'correctly completed a subcommand name') -}) + t.test('double dashes escape from flag completion', async t => { + process.env.COMP_CWORD = 2 + process.env.COMP_LINE = 'npm -- install --' + process.env.COMP_POINT = process.env.COMP_LINE.length -t.test('command completion errors propagate', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm access ' - process.env.COMP_POINT = process.env.COMP_LINE.length - accessCompletionError = true - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 - accessCompletionError = false - }) + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) - await t.rejects( - completion.exec(['npm', 'access', '']), - /access completion failed/, - 'catches the appropriate error' - ) - t.strictSame( - npmConfig, - { - argv: { - remain: ['npm', 'access'], - cooked: ['npm', 'access'], - original: ['npm', 'access'], - }, - }, - 'applies command config appropriately' - ) - t.strictSame(output, [], 'returns no results') -}) + await completion.exec(['npm', '--', 'install', '--']) -t.test('completion can complete flags', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm install --' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + t.matchSnapshot(outputs, 'full command list') }) - await completion.exec(['npm', 'install', '--']) + t.test('completion cannot complete options that take a value in mid-command', async t => { + process.env.COMP_CWORD = 2 + process.env.COMP_LINE = 'npm --registry install' + process.env.COMP_POINT = process.env.COMP_LINE.length - t.strictSame(npmConfig, {}, 'does not apply command config') - t.strictSame( - output, - [['--global', '--browser', '--registry', '--reg', '--no-global', '--no-browser'].join('\n')], - 'correctly completes flag names' - ) -}) + t.teardown(() => { + delete process.env.COMP_CWORD + delete process.env.COMP_LINE + delete process.env.COMP_POINT + }) -t.test('double dashes escape from flag completion', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm -- install --' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 + await completion.exec(['npm', '--registry', 'install']) + t.matchSnapshot(outputs, 'does not try to complete option arguments in the middle of a command') }) - - await completion.exec(['npm', '--', 'install', '--']) - - t.strictSame(npmConfig, {}, 'does not apply command config') - t.strictSame( - output, - [['access', 'adduser', 'completion', 'login'].join('\n')], - 'correctly completes flag names' - ) }) -t.test('completion cannot complete options that take a value in mid-command', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm --registry install' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - npm.config.clear() - output.length = 0 +t.test('windows without bash', async t => { + const { Npm, outputs } = mockNpm(t, { + '../../lib/utils/is-windows-shell.js': true, }) - - await completion.exec(['npm', '--registry', 'install']) - t.strictSame(npmConfig, {}, 'does not apply command config') - t.strictSame(output, [], 'does not try to complete option arguments in the middle of a command') + const npm = new Npm() + const completion = await npm.cmd('completion') + await t.rejects( + completion.exec({}), + { code: 'ENOTSUP', message: /completion supported only in MINGW/ }, + 'returns the correct error' + ) + t.matchSnapshot(outputs, 'no output') })