diff --git a/test/parallel/test-repl-tab-complete-buffer.js b/test/parallel/test-repl-tab-complete-buffer.js new file mode 100644 index 00000000000000..df288937df00c8 --- /dev/null +++ b/test/parallel/test-repl-tab-complete-buffer.js @@ -0,0 +1,73 @@ +'use strict'; + +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const { hijackStderr, restoreStderr } = require('../common/hijackstdio'); +const assert = require('assert'); + +const repl = require('repl'); + +const input = new ArrayStream(); +const replServer = repl.start({ + prompt: '', + input, + output: process.stdout, + allowBlockingCompletions: true, +}); + +// Some errors are passed to the domain, but do not callback +replServer._domain.on('error', assert.ifError); + +for (const type of [ + Array, + Buffer, + + Uint8Array, + Uint16Array, + Uint32Array, + + Uint8ClampedArray, + Int8Array, + Int16Array, + Int32Array, + Float32Array, + Float64Array, +]) { + input.run(['.clear']); + + if (type === Array) { + input.run([ + 'var ele = [];', + 'for (let i = 0; i < 1e6 + 1; i++) ele[i] = 0;', + 'ele.biu = 1;', + ]); + } else if (type === Buffer) { + input.run(['var ele = Buffer.alloc(1e6 + 1); ele.biu = 1;']); + } else { + input.run([`var ele = new ${type.name}(1e6 + 1); ele.biu = 1;`]); + } + + hijackStderr(common.mustNotCall()); + replServer.complete( + 'ele.', + common.mustCall((err, data) => { + restoreStderr(); + assert.ifError(err); + + const ele = + type === Array ? [] : type === Buffer ? Buffer.alloc(0) : new type(0); + + assert.strictEqual(data[0].includes('ele.biu'), true); + + data[0].forEach((key) => { + if (!key || key === 'ele.biu') return; + assert.notStrictEqual(ele[key.slice(4)], undefined); + }); + }) + ); +} + +// check Buffer.prototype.length not crashing. +// Refs: https://github.com/nodejs/node/pull/11961 +input.run(['.clear']); +replServer.complete('Buffer.prototype.', common.mustCall()); diff --git a/test/parallel/test-repl-tab-complete-custom-completer.js b/test/parallel/test-repl-tab-complete-custom-completer.js new file mode 100644 index 00000000000000..aee9538fc807ad --- /dev/null +++ b/test/parallel/test-repl-tab-complete-custom-completer.js @@ -0,0 +1,75 @@ +'use strict'; + +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); + +const repl = require('repl'); + +const putIn = new ArrayStream(); + +// To test custom completer function. +// Sync mode. +{ + const customCompletions = 'aaa aa1 aa2 bbb bb1 bb2 bb3 ccc ddd eee'.split(' '); + const testCustomCompleterSyncMode = repl.start({ + prompt: '', + input: putIn, + output: putIn, + completer: function completer(line) { + const hits = customCompletions.filter((c) => c.startsWith(line)); + // Show all completions if none found. + return [hits.length ? hits : customCompletions, line]; + } + }); + + // On empty line should output all the custom completions + // without complete anything. + testCustomCompleterSyncMode.complete('', common.mustCall((error, data) => { + assert.deepStrictEqual(data, [ + customCompletions, + '', + ]); + })); + + // On `a` should output `aaa aa1 aa2` and complete until `aa`. + testCustomCompleterSyncMode.complete('a', common.mustCall((error, data) => { + assert.deepStrictEqual(data, [ + 'aaa aa1 aa2'.split(' '), + 'a', + ]); + })); +} + +// To test custom completer function. +// Async mode. +{ + const customCompletions = 'aaa aa1 aa2 bbb bb1 bb2 bb3 ccc ddd eee'.split(' '); + const testCustomCompleterAsyncMode = repl.start({ + prompt: '', + input: putIn, + output: putIn, + completer: function completer(line, callback) { + const hits = customCompletions.filter((c) => c.startsWith(line)); + // Show all completions if none found. + callback(null, [hits.length ? hits : customCompletions, line]); + } + }); + + // On empty line should output all the custom completions + // without complete anything. + testCustomCompleterAsyncMode.complete('', common.mustCall((error, data) => { + assert.deepStrictEqual(data, [ + customCompletions, + '', + ]); + })); + + // On `a` should output `aaa aa1 aa2` and complete until `aa`. + testCustomCompleterAsyncMode.complete('a', common.mustCall((error, data) => { + assert.deepStrictEqual(data, [ + 'aaa aa1 aa2'.split(' '), + 'a', + ]); + })); +} diff --git a/test/parallel/test-repl-tab-complete-files.js b/test/parallel/test-repl-tab-complete-files.js new file mode 100644 index 00000000000000..35da204709c103 --- /dev/null +++ b/test/parallel/test-repl-tab-complete-files.js @@ -0,0 +1,80 @@ +'use strict'; + +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); +const path = require('path'); + +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('process.chdir is not available in Workers'); +} + +const repl = require('repl'); + +const replServer = repl.start({ + prompt: '', + input: new ArrayStream(), + output: process.stdout, + allowBlockingCompletions: true, +}); + +// Some errors are passed to the domain, but do not callback +replServer._domain.on('error', assert.ifError); + +// Tab completion for files/directories +{ + process.chdir(__dirname); + + const readFileSyncs = ['fs.readFileSync("', 'fs.promises.readFileSync("']; + if (!common.isWindows) { + readFileSyncs.forEach((readFileSync) => { + const fixturePath = `${readFileSync}../fixtures/test-repl-tab-completion`; + replServer.complete( + fixturePath, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.ok(data[0][0].includes('.hiddenfiles')); + assert.ok(data[0][1].includes('hellorandom.txt')); + assert.ok(data[0][2].includes('helloworld.js')); + }) + ); + + replServer.complete( + `${fixturePath}/hello`, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.ok(data[0][0].includes('hellorandom.txt')); + assert.ok(data[0][1].includes('helloworld.js')); + }) + ); + + replServer.complete( + `${fixturePath}/.h`, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.ok(data[0][0].includes('.hiddenfiles')); + }) + ); + + replServer.complete( + `${readFileSync}./xxxRandom/random`, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.strictEqual(data[0].length, 0); + }) + ); + + const testPath = fixturePath.slice(0, -1); + replServer.complete( + testPath, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.ok(data[0][0].includes('test-repl-tab-completion')); + assert.strictEqual(data[1], path.basename(testPath)); + }) + ); + }); + } +} diff --git a/test/parallel/test-repl-tab-complete-on-editor-mode.js b/test/parallel/test-repl-tab-complete-on-editor-mode.js index 610724de2a2844..a8a21ff8b13304 100644 --- a/test/parallel/test-repl-tab-complete-on-editor-mode.js +++ b/test/parallel/test-repl-tab-complete-on-editor-mode.js @@ -1,21 +1,48 @@ 'use strict'; -require('../common'); +const common = require('../common'); +const assert = require('assert'); const ArrayStream = require('../common/arraystream'); const repl = require('repl'); -const stream = new ArrayStream(); -const replServer = repl.start({ - input: stream, - output: stream, - terminal: true, -}); +// Tab completion in editor mode +{ + const editorStream = new ArrayStream(); + const editor = repl.start({ + stream: editorStream, + terminal: true, + useColors: false + }); -// Editor mode -replServer.write('.editor\n'); + editorStream.run(['.clear']); + editorStream.run(['.editor']); + + editor.completer('Uin', common.mustCall((_error, data) => { + assert.deepStrictEqual(data, [['Uint'], 'Uin']); + })); + + editorStream.run(['.clear']); + editorStream.run(['.editor']); + + editor.completer('var log = console.l', common.mustCall((_error, data) => { + assert.deepStrictEqual(data, [['console.log'], 'console.l']); + })); +} // Regression test for https://github.com/nodejs/node/issues/43528 -replServer.write('a'); -replServer.write(null, { name: 'tab' }); // Should not throw +{ + const stream = new ArrayStream(); + const replServer = repl.start({ + input: stream, + output: stream, + terminal: true, + }); + + // Editor mode + replServer.write('.editor\n'); + + replServer.write('a'); + replServer.write(null, { name: 'tab' }); // Should not throw -replServer.close(); + replServer.close(); +} diff --git a/test/parallel/test-repl-tab-complete-require.js b/test/parallel/test-repl-tab-complete-require.js new file mode 100644 index 00000000000000..9e9521af4d7de0 --- /dev/null +++ b/test/parallel/test-repl-tab-complete-require.js @@ -0,0 +1,212 @@ +'use strict'; + +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { builtinModules } = require('module'); +const publicModules = builtinModules.filter((lib) => !lib.startsWith('_')); + +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('process.chdir is not available in Workers'); +} + +// We have to change the directory to ../fixtures before requiring repl +// in order to make the tests for completion of node_modules work properly +// since repl modifies module.paths. +process.chdir(fixtures.fixturesDir); + +const repl = require('repl'); + +function prepareREPL() { + const replServer = repl.start({ + prompt: '', + input: new ArrayStream(), + output: process.stdout, + allowBlockingCompletions: true, + }); + + // Some errors are passed to the domain, but do not callback + replServer._domain.on('error', assert.ifError); + + return replServer; +} + +// Tab completion on require on builtin modules works +{ + const replServer = prepareREPL(); + + replServer.complete( + "require('", + common.mustCall(function(error, data) { + assert.strictEqual(error, null); + publicModules.forEach((lib) => { + assert( + data[0].includes(lib) && + (lib.startsWith('node:') || data[0].includes(`node:${lib}`)), + `${lib} not found` + ); + }); + const newModule = 'foobar'; + assert(!builtinModules.includes(newModule)); + repl.builtinModules.push(newModule); + replServer.complete( + "require('", + common.mustCall((_, [modules]) => { + assert.strictEqual(data[0].length + 1, modules.length); + assert(modules.includes(newModule)); + }) + ); + }) + ); +} + +// Tab completion on require on builtin modules works (with extra spaces and "n" prefix) +{ + const replServer = prepareREPL(); + + replServer.complete( + "require\t( 'n", + common.mustCall(function(error, data) { + assert.strictEqual(error, null); + assert.strictEqual(data.length, 2); + assert.strictEqual(data[1], 'n'); + // require(...) completions include `node:`-prefixed modules: + let lastIndex = -1; + + publicModules + .filter((lib) => !lib.startsWith('node:')) + .forEach((lib, index) => { + lastIndex = data[0].indexOf(`node:${lib}`); + assert.notStrictEqual(lastIndex, -1); + }); + assert.strictEqual(data[0][lastIndex + 1], ''); + // There is only one Node.js module that starts with n: + assert.strictEqual(data[0][lastIndex + 2], 'net'); + assert.strictEqual(data[0][lastIndex + 3], ''); + // It's possible to pick up non-core modules too + data[0].slice(lastIndex + 4).forEach((completion) => { + assert.match(completion, /^n/); + }); + }) + ); +} + +// Tab completion on require on external modules works +{ + const expected = ['@nodejsscope', '@nodejsscope/']; + + const replServer = prepareREPL(); + + // Require calls should handle all types of quotation marks. + for (const quotationMark of ["'", '"', '`']) { + replServer.complete( + 'require(`@nodejs', + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(data, [expected, '@nodejs']); + }) + ); + + // Completions should not be greedy in case the quotation ends. + const input = `require(${quotationMark}@nodejsscope${quotationMark}`; + replServer.complete( + input, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(data, [[], undefined]); + }) + ); + } +} + +{ + // Completions should find modules and handle whitespace after the opening bracket. + const replServer = prepareREPL(); + + replServer.complete( + 'require \t("no_ind', + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(data, [['no_index', 'no_index/'], 'no_ind']); + }) + ); +} + +// Test tab completion for require() relative to the current directory +{ + const replServer = prepareREPL(); + + const cwd = process.cwd(); + process.chdir(__dirname); + + ["require('.", 'require(".'].forEach((input) => { + replServer.complete( + input, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.strictEqual(data.length, 2); + assert.strictEqual(data[1], '.'); + assert.strictEqual(data[0].length, 2); + assert.ok(data[0].includes('./')); + assert.ok(data[0].includes('../')); + }) + ); + }); + + ["require('..", 'require("..'].forEach((input) => { + replServer.complete( + input, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(data, [['../'], '..']); + }) + ); + }); + + ['./', './test-'].forEach((path) => { + [`require('${path}`, `require("${path}`].forEach((input) => { + replServer.complete( + input, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.strictEqual(data.length, 2); + assert.strictEqual(data[1], path); + assert.ok(data[0].includes('./test-repl-tab-complete')); + }) + ); + }); + }); + + ['../parallel/', '../parallel/test-'].forEach((path) => { + [`require('${path}`, `require("${path}`].forEach((input) => { + replServer.complete( + input, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.strictEqual(data.length, 2); + assert.strictEqual(data[1], path); + assert.ok(data[0].includes('../parallel/test-repl-tab-complete')); + }) + ); + }); + }); + + { + const path = '../fixtures/repl-folder-extensions/f'; + replServer.complete( + `require('${path}`, + common.mustSucceed((data) => { + assert.strictEqual(data.length, 2); + assert.strictEqual(data[1], path); + assert.ok( + data[0].includes('../fixtures/repl-folder-extensions/foo.js') + ); + }) + ); + } + + process.chdir(cwd); +} diff --git a/test/parallel/test-repl-tab-complete.js b/test/parallel/test-repl-tab-complete.js index cbc82e5bf0296e..824a74600b4455 100644 --- a/test/parallel/test-repl-tab-complete.js +++ b/test/parallel/test-repl-tab-complete.js @@ -23,27 +23,8 @@ const common = require('../common'); const ArrayStream = require('../common/arraystream'); -const { - hijackStderr, - restoreStderr -} = require('../common/hijackstdio'); +const { describe, it } = require('node:test'); const assert = require('assert'); -const path = require('path'); -const fixtures = require('../common/fixtures'); -const { builtinModules } = require('module'); -const publicModules = builtinModules.filter((lib) => !lib.startsWith('_')); - -const hasInspector = process.features.inspector; -const { isMainThread } = require('worker_threads'); - -if (!isMainThread) { - common.skip('process.chdir is not available in Workers'); -} - -// We have to change the directory to ../fixtures before requiring repl -// in order to make the tests for completion of node_modules work properly -// since repl modifies module.paths. -process.chdir(fixtures.fixturesDir); const repl = require('repl'); @@ -53,722 +34,543 @@ function getNoResultsFunction() { }); } -// TODO: the following async IIFE and the completePromise function are necessary because -// the reply tests are all run against the same repl instance (testMe) and thus coordination -// needs to be in place for the tests not to interfere with each other, this is really -// not ideal, the tests in this file should be refactored so that each use its own isolated -// repl instance, making sure that no special coordination needs to be in place for them -// and also allowing the tests to all be run in parallel -(async () => { - const works = [['inner.one'], 'inner.o']; - const putIn = new ArrayStream(); - const testMe = repl.start({ +function prepareREPL() { + const input = new ArrayStream(); + const replServer = repl.start({ prompt: '', - input: putIn, + input, output: process.stdout, - allowBlockingCompletions: true + allowBlockingCompletions: true, }); - async function completePromise(query, callback) { - return new Promise((resolve) => { - testMe.complete(query, (...args) => { - callback(...args); - resolve(); - }); - }); - } - // Some errors are passed to the domain, but do not callback - testMe._domain.on('error', assert.ifError); - - // Tab Complete will not break in an object literal - putIn.run([ - 'var inner = {', - 'one:1', - ]); - await completePromise('inner.o', getNoResultsFunction()); - - await completePromise('console.lo', common.mustCall(function(error, data) { - assert.deepStrictEqual(data, [['console.log'], 'console.lo']); - })); - - await completePromise('console?.lo', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [['console?.log'], 'console?.lo']); - })); - - await completePromise('console?.zzz', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [[], 'console?.zzz']); - })); - - await completePromise('console?.', common.mustCall((error, data) => { - assert(data[0].includes('console?.log')); - assert.strictEqual(data[1], 'console?.'); - })); - - // Tab Complete will return globally scoped variables - putIn.run(['};']); - await completePromise('inner.o', common.mustCall(function(error, data) { - assert.deepStrictEqual(data, works); - })); - - putIn.run(['.clear']); - - // Tab Complete will not break in an ternary operator with () - putIn.run([ - 'var inner = ( true ', - '?', - '{one: 1} : ', - ]); - await completePromise('inner.o', getNoResultsFunction()); - - putIn.run(['.clear']); - - // Tab Complete will return a simple local variable - putIn.run([ - 'var top = function() {', - 'var inner = {one:1};', - ]); - await completePromise('inner.o', getNoResultsFunction()); - - // When you close the function scope tab complete will not return the - // locally scoped variable - putIn.run(['};']); - await completePromise('inner.o', getNoResultsFunction()); - - putIn.run(['.clear']); - - // Tab Complete will return a complex local variable - putIn.run([ - 'var top = function() {', - 'var inner = {', - ' one:1', - '};', - ]); - await completePromise('inner.o', getNoResultsFunction()); - - putIn.run(['.clear']); - - // Tab Complete will return a complex local variable even if the function - // has parameters - putIn.run([ - 'var top = function(one, two) {', - 'var inner = {', - ' one:1', - '};', - ]); - await completePromise('inner.o', getNoResultsFunction()); - - putIn.run(['.clear']); - - // Tab Complete will return a complex local variable even if the - // scope is nested inside an immediately executed function - putIn.run([ - 'var top = function() {', - '(function test () {', - 'var inner = {', - ' one:1', - '};', - ]); - await completePromise('inner.o', getNoResultsFunction()); - - putIn.run(['.clear']); - - // The definition has the params and { on a separate line. - putIn.run([ - 'var top = function() {', - 'r = function test (', - ' one, two) {', - 'var inner = {', - ' one:1', - '};', - ]); - await completePromise('inner.o', getNoResultsFunction()); - - putIn.run(['.clear']); - - // Currently does not work, but should not break, not the { - putIn.run([ - 'var top = function() {', - 'r = function test ()', - '{', - 'var inner = {', - ' one:1', - '};', - ]); - await completePromise('inner.o', getNoResultsFunction()); - - putIn.run(['.clear']); - - // Currently does not work, but should not break - putIn.run([ - 'var top = function() {', - 'r = function test (', - ')', - '{', - 'var inner = {', - ' one:1', - '};', - ]); - await completePromise('inner.o', getNoResultsFunction()); - - putIn.run(['.clear']); - - // Make sure tab completion works on non-Objects - putIn.run([ - 'var str = "test";', - ]); - // TODO - await completePromise('str.len', common.mustCall(function(error, data) { - assert.deepStrictEqual(data, [['str.length'], 'str.len']); - })); - - putIn.run(['.clear']); - - // Tab completion should be case-insensitive if member part is lower-case - putIn.run([ - 'var foo = { barBar: 1, BARbuz: 2, barBLA: 3 };', - ]); - await completePromise( - 'foo.b', - common.mustCall(function(error, data) { - assert.deepStrictEqual(data, [ - ['foo.BARbuz', 'foo.barBLA', 'foo.barBar'], - 'foo.b', - ]); - }) - ); - - putIn.run(['.clear']); - - // Tab completion should be case-insensitive if member part is upper-case - putIn.run([ - 'var foo = { barBar: 1, BARbuz: 2, barBLA: 3 };', - ]); - await completePromise( - 'foo.B', - common.mustCall(function(error, data) { - assert.deepStrictEqual(data, [ - ['foo.BARbuz', 'foo.barBLA', 'foo.barBar'], - 'foo.B', - ]); - }) - ); - - putIn.run(['.clear']); - - // Tab completion should not break on spaces - const spaceTimeout = setTimeout(function() { - throw new Error('timeout'); - }, 1000); - - await completePromise(' ', common.mustSucceed((data) => { - assert.strictEqual(data[1], ''); - assert.ok(data[0].includes('globalThis')); - clearTimeout(spaceTimeout); - })); - - // Tab completion should pick up the global "toString" object, and - // any other properties up the "global" object's prototype chain - await completePromise('toSt', common.mustCall(function(error, data) { - assert.deepStrictEqual(data, [['toString'], 'toSt']); - })); - - // Own properties should shadow properties on the prototype - putIn.run(['.clear']); - putIn.run([ - 'var x = Object.create(null);', - 'x.a = 1;', - 'x.b = 2;', - 'var y = Object.create(x);', - 'y.a = 3;', - 'y.c = 4;', - ]); - await completePromise('y.', common.mustCall(function(error, data) { - assert.deepStrictEqual(data, [['y.b', '', 'y.a', 'y.c'], 'y.']); - })); - - // Tab complete provides built in libs for require() - putIn.run(['.clear']); - - await completePromise('require(\'', common.mustCall(async function(error, data) { - assert.strictEqual(error, null); - publicModules.forEach((lib) => { - assert( - data[0].includes(lib) && (lib.startsWith('node:') || data[0].includes(`node:${lib}`)), - `${lib} not found` - ); - }); - const newModule = 'foobar'; - assert(!builtinModules.includes(newModule)); - repl.builtinModules.push(newModule); - await completePromise('require(\'', common.mustCall((_, [modules]) => { - assert.strictEqual(data[0].length + 1, modules.length); - assert(modules.includes(newModule)); - })); - })); - - await completePromise("require\t( 'n", common.mustCall(function(error, data) { - assert.strictEqual(error, null); - assert.strictEqual(data.length, 2); - assert.strictEqual(data[1], 'n'); - // require(...) completions include `node:`-prefixed modules: - let lastIndex = -1; - - publicModules.filter((lib) => !lib.startsWith('node:')).forEach((lib, index) => { - lastIndex = data[0].indexOf(`node:${lib}`); - assert.notStrictEqual(lastIndex, -1); - }); - assert.strictEqual(data[0][lastIndex + 1], ''); - // There is only one Node.js module that starts with n: - assert.strictEqual(data[0][lastIndex + 2], 'net'); - assert.strictEqual(data[0][lastIndex + 3], ''); - // It's possible to pick up non-core modules too - data[0].slice(lastIndex + 4).forEach((completion) => { - assert.match(completion, /^n/); - }); - })); - - { - const expected = ['@nodejsscope', '@nodejsscope/']; - // Require calls should handle all types of quotation marks. - for (const quotationMark of ["'", '"', '`']) { - putIn.run(['.clear']); - await completePromise('require(`@nodejs', common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.deepStrictEqual(data, [expected, '@nodejs']); - })); - - putIn.run(['.clear']); - // Completions should not be greedy in case the quotation ends. - const input = `require(${quotationMark}@nodejsscope${quotationMark}`; - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.deepStrictEqual(data, [[], undefined]); - })); - } - } - - { - putIn.run(['.clear']); - // Completions should find modules and handle whitespace after the opening - // bracket. - testMe.complete('require \t("no_ind', common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.deepStrictEqual(data, [['no_index', 'no_index/'], 'no_ind']); - })); - } - - // Test tab completion for require() relative to the current directory - { - putIn.run(['.clear']); - - const cwd = process.cwd(); - process.chdir(__dirname); - - ['require(\'.', 'require(".'].forEach((input) => { - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.strictEqual(data.length, 2); - assert.strictEqual(data[1], '.'); - assert.strictEqual(data[0].length, 2); - assert.ok(data[0].includes('./')); - assert.ok(data[0].includes('../')); - })); - }); + replServer._domain.on('error', assert.ifError); - ['require(\'..', 'require("..'].forEach((input) => { - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.deepStrictEqual(data, [['../'], '..']); - })); - }); + return { replServer, input }; +} - ['./', './test-'].forEach((path) => { - [`require('${path}`, `require("${path}`].forEach((input) => { - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.strictEqual(data.length, 2); - assert.strictEqual(data[1], path); - assert.ok(data[0].includes('./test-repl-tab-complete')); - })); - }); - }); +describe('REPL tab completion (core functionality)', () => { + it('does not break in an object literal', () => { + const { replServer, input } = prepareREPL(); - ['../parallel/', '../parallel/test-'].forEach((path) => { - [`require('${path}`, `require("${path}`].forEach((input) => { - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.strictEqual(data.length, 2); - assert.strictEqual(data[1], path); - assert.ok(data[0].includes('../parallel/test-repl-tab-complete')); - })); - }); - }); + input.run(['var inner = {', 'one:1']); - { - const path = '../fixtures/repl-folder-extensions/f'; - testMe.complete(`require('${path}`, common.mustSucceed((data) => { - assert.strictEqual(data.length, 2); - assert.strictEqual(data[1], path); - assert.ok(data[0].includes('../fixtures/repl-folder-extensions/foo.js')); - })); - } - - process.chdir(cwd); - } - - // Make sure tab completion works on context properties - putIn.run(['.clear']); - - putIn.run([ - 'var custom = "test";', - ]); - await completePromise('cus', common.mustCall(function(error, data) { - assert.deepStrictEqual(data, [['CustomEvent', 'custom'], 'cus']); - })); - - // Make sure tab completion doesn't crash REPL with half-baked proxy objects. - // See: https://github.com/nodejs/node/issues/2119 - putIn.run(['.clear']); - - putIn.run([ - 'var proxy = new Proxy({}, {ownKeys: () => { throw new Error(); }});', - ]); - - await completePromise('proxy.', common.mustCall(function(error, data) { - assert.strictEqual(error, null); - assert(Array.isArray(data)); - })); - - // Make sure tab completion does not include integer members of an Array - putIn.run(['.clear']); - - putIn.run(['var ary = [1,2,3];']); - await completePromise('ary.', common.mustCall(function(error, data) { - assert.strictEqual(data[0].includes('ary.0'), false); - assert.strictEqual(data[0].includes('ary.1'), false); - assert.strictEqual(data[0].includes('ary.2'), false); - })); - - // Make sure tab completion does not include integer keys in an object - putIn.run(['.clear']); - putIn.run(['var obj = {1:"a","1a":"b",a:"b"};']); - - await completePromise('obj.', common.mustCall(function(error, data) { - assert.strictEqual(data[0].includes('obj.1'), false); - assert.strictEqual(data[0].includes('obj.1a'), false); - assert(data[0].includes('obj.a')); - })); - - // Don't try to complete results of non-simple expressions - putIn.run(['.clear']); - putIn.run(['function a() {}']); - - await completePromise('a().b.', getNoResultsFunction()); - - // Works when prefixed with spaces - putIn.run(['.clear']); - putIn.run(['var obj = {1:"a","1a":"b",a:"b"};']); - - await completePromise(' obj.', common.mustCall((error, data) => { - assert.strictEqual(data[0].includes('obj.1'), false); - assert.strictEqual(data[0].includes('obj.1a'), false); - assert(data[0].includes('obj.a')); - })); - - // Works inside assignments - putIn.run(['.clear']); - - await completePromise('var log = console.lo', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [['console.log'], 'console.lo']); - })); - - // Tab completion for defined commands - putIn.run(['.clear']); - - await completePromise('.b', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [['break'], 'b']); - })); - putIn.run(['.clear']); - putIn.run(['var obj = {"hello, world!": "some string", "key": 123}']); - await completePromise('obj.', common.mustCall((error, data) => { - assert.strictEqual(data[0].includes('obj.hello, world!'), false); - assert(data[0].includes('obj.key')); - })); - - // Make sure tab completion does not include __defineSetter__ and friends. - putIn.run(['.clear']); - - putIn.run(['var obj = {};']); - await completePromise('obj.', common.mustCall(function(error, data) { - assert.strictEqual(data[0].includes('obj.__defineGetter__'), false); - assert.strictEqual(data[0].includes('obj.__defineSetter__'), false); - assert.strictEqual(data[0].includes('obj.__lookupGetter__'), false); - assert.strictEqual(data[0].includes('obj.__lookupSetter__'), false); - assert.strictEqual(data[0].includes('obj.__proto__'), true); - })); - - // Tab completion for files/directories - { - putIn.run(['.clear']); - process.chdir(__dirname); - - const readFileSyncs = ['fs.readFileSync("', 'fs.promises.readFileSync("']; - if (!common.isWindows) { - readFileSyncs.forEach((readFileSync) => { - const fixturePath = `${readFileSync}../fixtures/test-repl-tab-completion`; - testMe.complete(fixturePath, common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.ok(data[0][0].includes('.hiddenfiles')); - assert.ok(data[0][1].includes('hellorandom.txt')); - assert.ok(data[0][2].includes('helloworld.js')); - })); - - testMe.complete(`${fixturePath}/hello`, - common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.ok(data[0][0].includes('hellorandom.txt')); - assert.ok(data[0][1].includes('helloworld.js')); - }) - ); - - testMe.complete(`${fixturePath}/.h`, - common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.ok(data[0][0].includes('.hiddenfiles')); - }) - ); - - testMe.complete(`${readFileSync}./xxxRandom/random`, - common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.strictEqual(data[0].length, 0); - }) - ); - - const testPath = fixturePath.slice(0, -1); - testMe.complete(testPath, common.mustCall((err, data) => { - assert.strictEqual(err, null); - assert.ok(data[0][0].includes('test-repl-tab-completion')); - assert.strictEqual( - data[1], - path.basename(testPath) - ); - })); - }); - } - } - - for (const type of [ - Array, - Buffer, - - Uint8Array, - Uint16Array, - Uint32Array, - - Uint8ClampedArray, - Int8Array, - Int16Array, - Int32Array, - Float32Array, - Float64Array, - ]) { - putIn.run(['.clear']); - - if (type === Array) { - putIn.run([ - 'var ele = [];', - 'for (let i = 0; i < 1e6 + 1; i++) ele[i] = 0;', - 'ele.biu = 1;', - ]); - } else if (type === Buffer) { - putIn.run(['var ele = Buffer.alloc(1e6 + 1); ele.biu = 1;']); - } else { - putIn.run([`var ele = new ${type.name}(1e6 + 1); ele.biu = 1;`]); - } - - hijackStderr(common.mustNotCall()); - await completePromise('ele.', common.mustCall((err, data) => { - restoreStderr(); - assert.ifError(err); - - const ele = (type === Array) ? - [] : - (type === Buffer ? - Buffer.alloc(0) : - new type(0)); - - assert.strictEqual(data[0].includes('ele.biu'), true); - - data[0].forEach((key) => { - if (!key || key === 'ele.biu') return; - assert.notStrictEqual(ele[key.slice(4)], undefined); - }); - })); - }; - - // check Buffer.prototype.length not crashing. - // Refs: https://github.com/nodejs/node/pull/11961 - putIn.run(['.clear']); - await completePromise('Buffer.prototype.', common.mustCall()); - - // Make sure repl gives correct autocomplete on literals - await completePromise('``.a', common.mustCall((err, data) => { - assert.strictEqual(data[0].includes('``.at'), true); - })); - await completePromise('\'\'.a', common.mustCall((err, data) => { - assert.strictEqual(data[0].includes('\'\'.at'), true); - })); - await completePromise('"".a', common.mustCall((err, data) => { - assert.strictEqual(data[0].includes('"".at'), true); - })); - await completePromise('("").a', common.mustCall((err, data) => { - assert.strictEqual(data[0].includes('("").at'), true); - })); - await completePromise('[].a', common.mustCall((err, data) => { - assert.strictEqual(data[0].includes('[].at'), true); - })); - await completePromise('{}.a', common.mustCall((err, data) => { - assert.deepStrictEqual(data[0], []); - })); + replServer.complete('inner.o', getNoResultsFunction()); -})().then(common.mustCall()); + replServer.complete( + 'console.lo', + common.mustCall(function(_error, data) { + assert.deepStrictEqual(data, [['console.log'], 'console.lo']); + }) + ); -const putIn = new ArrayStream(); + replServer.close(); + }); -const testNonGlobal = repl.start({ - input: putIn, - output: putIn, - useGlobal: false -}); + it('works with optional chaining', () => { + const { replServer } = prepareREPL(); + + replServer.complete( + 'console?.lo', + common.mustCall((_error, data) => { + assert.deepStrictEqual(data, [['console?.log'], 'console?.lo']); + }) + ); + + replServer.complete( + 'console?.zzz', + common.mustCall((_error, data) => { + assert.deepStrictEqual(data, [[], 'console?.zzz']); + }) + ); + + replServer.complete( + 'console?.', + common.mustCall((_error, data) => { + assert(data[0].includes('console?.log')); + assert.strictEqual(data[1], 'console?.'); + }) + ); + + replServer.close(); + }); -const builtins = [ - [ - 'if', - 'import', - 'in', - 'instanceof', - '', - 'Infinity', - 'Int16Array', - 'Int32Array', - 'Int8Array', - ...(common.hasIntl ? ['Intl'] : []), - 'Iterator', - 'inspector', - 'isFinite', - 'isNaN', - '', - 'isPrototypeOf', - ], - 'I', -]; - -testNonGlobal.complete('I', common.mustCall((error, data) => { - assert.deepStrictEqual(data, builtins); -})); - -// To test custom completer function. -// Sync mode. -const customCompletions = 'aaa aa1 aa2 bbb bb1 bb2 bb3 ccc ddd eee'.split(' '); -const testCustomCompleterSyncMode = repl.start({ - prompt: '', - input: putIn, - output: putIn, - completer: function completer(line) { - const hits = customCompletions.filter((c) => c.startsWith(line)); - // Show all completions if none found. - return [hits.length ? hits : customCompletions, line]; - } -}); + it('returns object completions', () => { + const { replServer, input } = prepareREPL(); -// On empty line should output all the custom completions -// without complete anything. -testCustomCompleterSyncMode.complete('', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [ - customCompletions, - '', - ]); -})); - -// On `a` should output `aaa aa1 aa2` and complete until `aa`. -testCustomCompleterSyncMode.complete('a', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [ - 'aaa aa1 aa2'.split(' '), - 'a', - ]); -})); - -// To test custom completer function. -// Async mode. -const testCustomCompleterAsyncMode = repl.start({ - prompt: '', - input: putIn, - output: putIn, - completer: function completer(line, callback) { - const hits = customCompletions.filter((c) => c.startsWith(line)); - // Show all completions if none found. - callback(null, [hits.length ? hits : customCompletions, line]); - } -}); + input.run(['var inner = {', 'one:1']); -// On empty line should output all the custom completions -// without complete anything. -testCustomCompleterAsyncMode.complete('', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [ - customCompletions, - '', - ]); -})); - -// On `a` should output `aaa aa1 aa2` and complete until `aa`. -testCustomCompleterAsyncMode.complete('a', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [ - 'aaa aa1 aa2'.split(' '), - 'a', - ]); -})); - -// Tab completion in editor mode -const editorStream = new ArrayStream(); -const editor = repl.start({ - stream: editorStream, - terminal: true, - useColors: false -}); + input.run(['};']); + + replServer.complete( + 'inner.o', + common.mustCall(function(_error, data) { + assert.deepStrictEqual(data, [['inner.one'], 'inner.o']); + }) + ); -editorStream.run(['.clear']); -editorStream.run(['.editor']); - -editor.completer('Uin', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [['Uint'], 'Uin']); -})); - -editorStream.run(['.clear']); -editorStream.run(['.editor']); - -editor.completer('var log = console.l', common.mustCall((error, data) => { - assert.deepStrictEqual(data, [['console.log'], 'console.l']); -})); - -{ - // Tab completion of lexically scoped variables - const stream = new ArrayStream(); - const testRepl = repl.start({ stream }); - - stream.run([` - let lexicalLet = true; - const lexicalConst = true; - class lexicalKlass {} - `]); - - ['Let', 'Const', 'Klass'].forEach((type) => { - const query = `lexical${type[0]}`; - const expected = hasInspector ? [[`lexical${type}`], query] : - [[], `lexical${type[0]}`]; - testRepl.complete(query, common.mustCall((error, data) => { - assert.deepStrictEqual(data, expected); - })); + replServer.close(); }); -} + + it('does not break in a ternary operator with ()', () => { + const { replServer, input } = prepareREPL(); + + input.run(['var inner = ( true ', '?', '{one: 1} : ']); + + replServer.complete('inner.o', getNoResultsFunction()); + + replServer.close(); + }); + + it('works on literals', () => { + const { replServer } = prepareREPL(); + + replServer.complete( + '``.a', + common.mustCall((err, data) => { + assert.strictEqual(data[0].includes('``.at'), true); + }) + ); + replServer.complete( + "''.a", + common.mustCall((err, data) => { + assert.strictEqual(data[0].includes("''.at"), true); + }) + ); + replServer.complete( + '"".a', + common.mustCall((err, data) => { + assert.strictEqual(data[0].includes('"".at'), true); + }) + ); + replServer.complete( + '("").a', + common.mustCall((err, data) => { + assert.strictEqual(data[0].includes('("").at'), true); + }) + ); + replServer.complete( + '[].a', + common.mustCall((err, data) => { + assert.strictEqual(data[0].includes('[].at'), true); + }) + ); + replServer.complete( + '{}.a', + common.mustCall((err, data) => { + assert.deepStrictEqual(data[0], []); + }) + ); + + replServer.close(); + }); + + it("does not return a function's local variable", () => { + const { replServer, input } = prepareREPL(); + + input.run(['var top = function() {', 'var inner = {one:1};', '}']); + + replServer.complete('inner.o', getNoResultsFunction()); + + replServer.close(); + }); + + it("does not return a function's local variable even when the function has parameters", () => { + const { replServer, input } = prepareREPL(); + + input.run([ + 'var top = function(one, two) {', + 'var inner = {', + ' one:1', + '};', + ]); + + replServer.complete('inner.o', getNoResultsFunction()); + + replServer.close(); + }); + + it("does not return a function's local variable" + + 'even if the scope is nested inside an immediately executed function', () => { + const { replServer, input } = prepareREPL(); + + input.run([ + 'var top = function() {', + '(function test () {', + 'var inner = {', + ' one:1', + '};', + ]); + + replServer.complete('inner.o', getNoResultsFunction()); + + replServer.close(); + }); + + it("does not return a function's local variable" + + 'even if the scope is nested inside an immediately executed function' + + '(the definition has the params and { on a separate line)', () => { + const { replServer, input } = prepareREPL(); + + input.run([ + 'var top = function() {', + 'r = function test (', + ' one, two) {', + 'var inner = {', + ' one:1', + '};', + ]); + + replServer.complete('inner.o', getNoResultsFunction()); + + replServer.close(); + }); + + it('currently does not work, but should not break (local inner)', () => { + const { replServer, input } = prepareREPL(); + + input.run([ + 'var top = function() {', + 'r = function test ()', + '{', + 'var inner = {', + ' one:1', + '};', + ]); + + replServer.complete('inner.o', getNoResultsFunction()); + + replServer.close(); + }); + + it('currently does not work, but should not break (local inner parens next line)', () => { + const { replServer, input } = prepareREPL(); + + input.run([ + 'var top = function() {', + 'r = function test (', + ')', + '{', + 'var inner = {', + ' one:1', + '};', + ]); + + replServer.complete('inner.o', getNoResultsFunction()); + + replServer.close(); + }); + + it('works on non-Objects', () => { + const { replServer, input } = prepareREPL(); + + input.run(['var str = "test";']); + + replServer.complete( + 'str.len', + common.mustCall(function(_error, data) { + assert.deepStrictEqual(data, [['str.length'], 'str.len']); + }) + ); + + replServer.close(); + }); + + it('should be case-insensitive if member part is lower-case', () => { + const { replServer, input } = prepareREPL(); + + input.run(['var foo = { barBar: 1, BARbuz: 2, barBLA: 3 };']); + + replServer.complete( + 'foo.b', + common.mustCall(function(_error, data) { + assert.deepStrictEqual(data, [ + ['foo.BARbuz', 'foo.barBLA', 'foo.barBar'], + 'foo.b', + ]); + }) + ); + + replServer.close(); + }); + + it('should be case-insensitive if member part is upper-case', () => { + const { replServer, input } = prepareREPL(); + + input.run(['var foo = { barBar: 1, BARbuz: 2, barBLA: 3 };']); + + replServer.complete( + 'foo.B', + common.mustCall(function(_error, data) { + assert.deepStrictEqual(data, [ + ['foo.BARbuz', 'foo.barBLA', 'foo.barBar'], + 'foo.B', + ]); + }) + ); + + replServer.close(); + }); + + it('should not break on spaces', () => { + const { replServer } = prepareREPL(); + + const spaceTimeout = setTimeout(function() { + throw new Error('timeout'); + }, 1000); + + replServer.complete( + ' ', + common.mustSucceed((data) => { + assert.strictEqual(data[1], ''); + assert.ok(data[0].includes('globalThis')); + clearTimeout(spaceTimeout); + }) + ); + + replServer.close(); + }); + + it(`should pick up the global "toString" object, and any other properties up the "global" object's prototype chain`, () => { + const { replServer } = prepareREPL(); + + replServer.complete( + 'toSt', + common.mustCall(function(_error, data) { + assert.deepStrictEqual(data, [['toString'], 'toSt']); + }) + ); + + replServer.close(); + }); + + it('should make own properties shadow properties on the prototype', () => { + const { replServer, input } = prepareREPL(); + + input.run([ + 'var x = Object.create(null);', + 'x.a = 1;', + 'x.b = 2;', + 'var y = Object.create(x);', + 'y.a = 3;', + 'y.c = 4;', + ]); + + replServer.complete( + 'y.', + common.mustCall(function(_error, data) { + assert.deepStrictEqual(data, [['y.b', '', 'y.a', 'y.c'], 'y.']); + }) + ); + + replServer.close(); + }); + + it('works on context properties', () => { + const { replServer, input } = prepareREPL(); + + input.run(['var custom = "test";']); + + replServer.complete( + 'cus', + common.mustCall(function(_error, data) { + assert.deepStrictEqual(data, [['CustomEvent', 'custom'], 'cus']); + }) + ); + + replServer.close(); + }); + + it("doesn't crash REPL with half-baked proxy objects", () => { + const { replServer, input } = prepareREPL(); + + input.run([ + 'var proxy = new Proxy({}, {ownKeys: () => { throw new Error(); }});', + ]); + + replServer.complete( + 'proxy.', + common.mustCall(function(error, data) { + assert.strictEqual(error, null); + assert(Array.isArray(data)); + }) + ); + + replServer.close(); + }); + + it('does not include integer members of an Array', () => { + const { replServer, input } = prepareREPL(); + + input.run(['var ary = [1,2,3];']); + + replServer.complete( + 'ary.', + common.mustCall(function(_error, data) { + assert.strictEqual(data[0].includes('ary.0'), false); + assert.strictEqual(data[0].includes('ary.1'), false); + assert.strictEqual(data[0].includes('ary.2'), false); + }) + ); + + replServer.close(); + }); + + it('does not include integer keys in an object', () => { + const { replServer, input } = prepareREPL(); + + input.run(['var obj = {1:"a","1a":"b",a:"b"};']); + + replServer.complete( + 'obj.', + common.mustCall(function(_error, data) { + assert.strictEqual(data[0].includes('obj.1'), false); + assert.strictEqual(data[0].includes('obj.1a'), false); + assert(data[0].includes('obj.a')); + }) + ); + + replServer.close(); + }); + + it('does not try to complete results of non-simple expressions', () => { + const { replServer, input } = prepareREPL(); + + input.run(['function a() {}']); + + replServer.complete('a().b.', getNoResultsFunction()); + + replServer.close(); + }); + + it('works when prefixed with spaces', () => { + const { replServer, input } = prepareREPL(); + + input.run(['var obj = {1:"a","1a":"b",a:"b"};']); + + replServer.complete( + ' obj.', + common.mustCall((_error, data) => { + assert.strictEqual(data[0].includes('obj.1'), false); + assert.strictEqual(data[0].includes('obj.1a'), false); + assert(data[0].includes('obj.a')); + }) + ); + + replServer.close(); + }); + + it('works inside assignments', () => { + const { replServer } = prepareREPL(); + + replServer.complete( + 'var log = console.lo', + common.mustCall((_error, data) => { + assert.deepStrictEqual(data, [['console.log'], 'console.lo']); + }) + ); + + replServer.close(); + }); + + it('works for defined commands', () => { + const { replServer, input } = prepareREPL(); + + replServer.complete( + '.b', + common.mustCall((error, data) => { + assert.deepStrictEqual(data, [['break'], 'b']); + }) + ); + + input.run(['var obj = {"hello, world!": "some string", "key": 123}']); + + replServer.complete( + 'obj.', + common.mustCall((error, data) => { + assert.strictEqual(data[0].includes('obj.hello, world!'), false); + assert(data[0].includes('obj.key')); + }) + ); + + replServer.close(); + }); + + it('does not include __defineSetter__ and friends', () => { + const { replServer, input } = prepareREPL(); + + input.run(['var obj = {};']); + + replServer.complete( + 'obj.', + common.mustCall(function(error, data) { + assert.strictEqual(data[0].includes('obj.__defineGetter__'), false); + assert.strictEqual(data[0].includes('obj.__defineSetter__'), false); + assert.strictEqual(data[0].includes('obj.__lookupGetter__'), false); + assert.strictEqual(data[0].includes('obj.__lookupSetter__'), false); + assert.strictEqual(data[0].includes('obj.__proto__'), true); + }) + ); + + replServer.close(); + }); + + it('works with builtin values', () => { + const { replServer } = prepareREPL(); + + replServer.complete( + 'I', + common.mustCall((error, data) => { + assert.deepStrictEqual(data, [ + [ + 'if', + 'import', + 'in', + 'instanceof', + '', + 'Infinity', + 'Int16Array', + 'Int32Array', + 'Int8Array', + ...(common.hasIntl ? ['Intl'] : []), + 'Iterator', + 'inspector', + 'isFinite', + 'isNaN', + '', + 'isPrototypeOf', + ], + 'I', + ]); + }) + ); + + replServer.close(); + }); + + it('works with lexically scoped variables', () => { + const { replServer, input } = prepareREPL(); + + input.run([ + 'let lexicalLet = true;', + 'const lexicalConst = true;', + 'class lexicalKlass {}', + ]); + + ['Let', 'Const', 'Klass'].forEach((type) => { + const query = `lexical${type[0]}`; + const hasInspector = process.features.inspector; + const expected = hasInspector ? + [[`lexical${type}`], query] : + [[], `lexical${type[0]}`]; + replServer.complete( + query, + common.mustCall((error, data) => { + assert.deepStrictEqual(data, expected); + }) + ); + }); + + replServer.close(); + }); +});