diff --git a/lib/repl.js b/lib/repl.js index d500181736b7ac..547b3e29adc07d 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -310,7 +310,6 @@ function REPLServer(prompt, options.useColors = shouldColorize(options.output); } - // TODO(devsnek): Add a test case for custom eval functions. const preview = options.terminal && (options.preview !== undefined ? !!options.preview : !eval_); diff --git a/test/parallel/test-repl-custom-eval-previews.js b/test/parallel/test-repl-custom-eval-previews.js new file mode 100644 index 00000000000000..1d709109aeb194 --- /dev/null +++ b/test/parallel/test-repl-custom-eval-previews.js @@ -0,0 +1,101 @@ +'use strict'; + +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); +const { describe, it } = require('node:test'); + +common.skipIfInspectorDisabled(); + +const repl = require('repl'); + +const testingReplPrompt = '_REPL_TESTING_PROMPT_>'; + +// Processes some input in a REPL instance and returns a promise that +// resolves to the produced output (as a string). +function getReplRunOutput(input, replOptions) { + return new Promise((resolve) => { + const inputStream = new ArrayStream(); + const outputStream = new ArrayStream(); + + const replServer = repl.start({ + input: inputStream, + output: outputStream, + prompt: testingReplPrompt, + ...replOptions, + }); + + let output = ''; + + outputStream.write = (chunk) => { + output += chunk; + // The prompt appears after the input has been processed + if (output.includes(testingReplPrompt)) { + replServer.close(); + resolve(output); + } + }; + + inputStream.emit('data', input); + + inputStream.run(['']); + }); +} + +describe('with previews', () => { + it("doesn't show previews by default", async () => { + const input = "'Hello custom' + ' eval World!'"; + const output = await getReplRunOutput( + input, + { + terminal: true, + eval: (code, _ctx, _replRes, cb) => cb(null, eval(code)), + }, + ); + const lines = getSingleCommandLines(output); + assert.match(lines.command, /^'Hello custom' \+ ' eval World!'/); + assert.match(lines.prompt, new RegExp(`${testingReplPrompt}$`)); + assert.strictEqual(lines.result, "'Hello custom eval World!'"); + assert.strictEqual(lines.preview, undefined); + }); + + it('does show previews if `preview` is set to `true`', async () => { + const input = "'Hello custom' + ' eval World!'"; + const output = await getReplRunOutput( + input, + { + terminal: true, + eval: (code, _ctx, _replRes, cb) => cb(null, eval(code)), + preview: true, + }, + ); + const lines = getSingleCommandLines(output); + assert.match(lines.command, /^'Hello custom' \+ ' eval World!'/); + assert.match(lines.prompt, new RegExp(`${testingReplPrompt}$`)); + assert.strictEqual(lines.result, "'Hello custom eval World!'"); + assert.match(lines.preview, /'Hello custom eval World!'/); + }); +}); + +function getSingleCommandLines(output) { + const outputLines = output.split('\n'); + + // The first line contains the command being run + const command = outputLines.shift(); + + // The last line contains the prompt (asking for some new input) + const prompt = outputLines.pop(); + + // The line before the last one contains the result of the command + const result = outputLines.pop(); + + // The line before that contains the preview of the command + const preview = outputLines.shift(); + + return { + command, + prompt, + result, + preview, + }; +} diff --git a/test/parallel/test-repl-custom-eval.js b/test/parallel/test-repl-custom-eval.js new file mode 100644 index 00000000000000..46890b2b5d1c84 --- /dev/null +++ b/test/parallel/test-repl-custom-eval.js @@ -0,0 +1,121 @@ +'use strict'; + +require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); +const { describe, it } = require('node:test'); + +const repl = require('repl'); + +// Processes some input in a REPL instance and returns a promise that +// resolves to the produced output (as a string). +function getReplRunOutput(input, replOptions) { + return new Promise((resolve) => { + const inputStream = new ArrayStream(); + const outputStream = new ArrayStream(); + + const testingReplPrompt = '_REPL_TESTING_PROMPT_>'; + + const replServer = repl.start({ + input: inputStream, + output: outputStream, + prompt: testingReplPrompt, + ...replOptions, + }); + + let output = ''; + + outputStream.write = (chunk) => { + output += chunk; + // The prompt appears after the input has been processed + if (output.includes(testingReplPrompt)) { + replServer.close(); + resolve(output); + } + }; + + inputStream.emit('data', input); + + inputStream.run(['']); + }); +} + +describe('repl with custom eval', { concurrency: true }, () => { + it('uses the custom eval function as expected', async () => { + const output = await getReplRunOutput('Convert this to upper case', { + terminal: true, + eval: (code, _ctx, _replRes, cb) => cb(null, code.toUpperCase()), + }); + assert.match( + output, + /Convert this to upper case\r\n'CONVERT THIS TO UPPER CASE\\n'/ + ); + }); + + it('surfaces errors as expected', async () => { + const output = await getReplRunOutput('Convert this to upper case', { + terminal: true, + eval: (_code, _ctx, _replRes, cb) => cb(new Error('Testing Error')), + }); + assert.match(output, /Uncaught Error: Testing Error\n/); + }); + + it('provides a repl context to the eval callback', async () => { + const context = await new Promise((resolve) => { + const r = repl.start({ + eval: (_cmd, context) => resolve(context), + }); + r.context = { foo: 'bar' }; + r.write('\n.exit\n'); + }); + assert.strictEqual(context.foo, 'bar'); + }); + + it('provides the global context to the eval callback', async () => { + const context = await new Promise((resolve) => { + const r = repl.start({ + useGlobal: true, + eval: (_cmd, context) => resolve(context), + }); + global.foo = 'global_foo'; + r.write('\n.exit\n'); + }); + + assert.strictEqual(context.foo, 'global_foo'); + delete global.foo; + }); + + it('inherits variables from the global context but does not use it afterwords if `useGlobal` is false', async () => { + global.bar = 'global_bar'; + const context = await new Promise((resolve) => { + const r = repl.start({ + useGlobal: false, + eval: (_cmd, context) => resolve(context), + }); + global.baz = 'global_baz'; + r.write('\n.exit\n'); + }); + + assert.strictEqual(context.bar, 'global_bar'); + assert.notStrictEqual(context.baz, 'global_baz'); + delete global.bar; + delete global.baz; + }); + + /** + * Default preprocessor transforms + * function f() {} to + * var f = function f() {} + * This test ensures that original input is preserved. + * Reference: https://github.com/nodejs/node/issues/9743 + */ + it('preserves the original input', async () => { + const cmd = await new Promise((resolve) => { + const r = repl.start({ + eval: (cmd) => resolve(cmd), + }); + r.write('function f() {}\n.exit\n'); + }); + assert.strictEqual(cmd, 'function f() {}\n'); + }); +}); diff --git a/test/parallel/test-repl-eval.js b/test/parallel/test-repl-eval.js deleted file mode 100644 index d775423fb74a52..00000000000000 --- a/test/parallel/test-repl-eval.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; -const common = require('../common'); -const assert = require('assert'); -const repl = require('repl'); - -{ - let evalCalledWithExpectedArgs = false; - - const options = { - eval: common.mustCall((cmd, context) => { - // Assertions here will not cause the test to exit with an error code - // so set a boolean that is checked later instead. - evalCalledWithExpectedArgs = (cmd === 'function f() {}\n' && - context.foo === 'bar'); - }) - }; - - const r = repl.start(options); - r.context = { foo: 'bar' }; - - try { - // Default preprocessor transforms - // function f() {} to - // var f = function f() {} - // Test to ensure that original input is preserved. - // Reference: https://github.com/nodejs/node/issues/9743 - r.write('function f() {}\n'); - } finally { - r.write('.exit\n'); - } - - assert(evalCalledWithExpectedArgs); -}