diff options
-rw-r--r-- | doc/api/repl.md | 5 | ||||
-rw-r--r-- | lib/internal/repl/utils.js | 168 | ||||
-rw-r--r-- | lib/repl.js | 18 | ||||
-rw-r--r-- | test/parallel/test-repl-history-navigation.js | 28 | ||||
-rw-r--r-- | test/parallel/test-repl-multiline.js | 62 | ||||
-rw-r--r-- | test/parallel/test-repl-preview.js | 131 | ||||
-rw-r--r-- | test/parallel/test-repl-top-level-await.js | 101 |
7 files changed, 442 insertions, 71 deletions
diff --git a/doc/api/repl.md b/doc/api/repl.md index 967336710c..21f5193c12 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -510,6 +510,9 @@ with REPL instances programmatically. <!-- YAML added: v0.1.91 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/30811 + description: The `preview` option is now available. - version: v12.0.0 pr-url: https://github.com/nodejs/node/pull/26518 description: The `terminal` option now follows the default description in @@ -562,6 +565,8 @@ changes: * `breakEvalOnSigint` {boolean} Stop evaluating the current piece of code when `SIGINT` is received, such as when `Ctrl+C` is pressed. This cannot be used together with a custom `eval` function. **Default:** `false`. + * `preview` {boolean} Defines if the repl prints output previews or not. + **Default:** `true`. Always `false` in case `terminal` is falsy. * Returns: {repl.REPLServer} The `repl.start()` method creates and starts a [`repl.REPLServer`][] instance. diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index 6c011e8533..c4280c1d1f 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -1,10 +1,12 @@ 'use strict'; const { + MathMin, Symbol, } = primordials; -const acorn = require('internal/deps/acorn/acorn/dist/acorn'); +const { tokTypes: tt, Parser: AcornParser } = + require('internal/deps/acorn/acorn/dist/acorn'); const privateMethods = require('internal/deps/acorn-plugins/acorn-private-methods/index'); const classFields = @@ -13,7 +15,30 @@ const numericSeparator = require('internal/deps/acorn-plugins/acorn-numeric-separator/index'); const staticClassFeatures = require('internal/deps/acorn-plugins/acorn-static-class-features/index'); -const { tokTypes: tt, Parser: AcornParser } = acorn; + +const { sendInspectorCommand } = require('internal/util/inspector'); + +const { + ERR_INSPECTOR_NOT_AVAILABLE +} = require('internal/errors').codes; + +const { + clearLine, + cursorTo, + moveCursor, +} = require('readline'); + +const { inspect } = require('util'); + +const debug = require('internal/util/debuglog').debuglog('repl'); + +const inspectOptions = { + depth: 1, + colors: false, + compact: true, + breakLength: Infinity +}; +const inspectedOptions = inspect(inspectOptions, { colors: false }); // If the error is that we've unexpectedly ended the input, // then let the user try to recover by adding more input. @@ -91,7 +116,144 @@ function isRecoverableError(e, code) { } } +function setupPreview(repl, contextSymbol, bufferSymbol, active) { + // Simple terminals can't handle previews. + if (process.env.TERM === 'dumb' || !active) { + return { showInputPreview() {}, clearPreview() {} }; + } + + let preview = null; + let lastPreview = ''; + + const clearPreview = () => { + if (preview !== null) { + moveCursor(repl.output, 0, 1); + clearLine(repl.output); + moveCursor(repl.output, 0, -1); + lastPreview = preview; + preview = null; + } + }; + + // This returns a code preview for arbitrary input code. + function getPreviewInput(input, callback) { + // For similar reasons as `defaultEval`, wrap expressions starting with a + // curly brace with parenthesis. + if (input.startsWith('{') && !input.endsWith(';')) { + input = `(${input})`; + } + sendInspectorCommand((session) => { + session.post('Runtime.evaluate', { + expression: input, + throwOnSideEffect: true, + timeout: 333, + contextId: repl[contextSymbol], + }, (error, preview) => { + if (error) { + callback(error); + return; + } + const { result } = preview; + if (result.value !== undefined) { + callback(null, inspect(result.value, inspectOptions)); + // Ignore EvalErrors, SyntaxErrors and ReferenceErrors. It is not clear + // where they came from and if they are recoverable or not. Other errors + // may be inspected. + } else if (preview.exceptionDetails && + (result.className === 'EvalError' || + result.className === 'SyntaxError' || + result.className === 'ReferenceError')) { + callback(null, null); + } else if (result.objectId) { + session.post('Runtime.callFunctionOn', { + functionDeclaration: `(v) => util.inspect(v, ${inspectedOptions})`, + objectId: result.objectId, + arguments: [result] + }, (error, preview) => { + if (error) { + callback(error); + } else { + callback(null, preview.result.value); + } + }); + } else { + // Either not serializable or undefined. + callback(null, result.unserializableValue || result.type); + } + }); + }, () => callback(new ERR_INSPECTOR_NOT_AVAILABLE())); + } + + const showInputPreview = () => { + // Prevent duplicated previews after a refresh. + if (preview !== null) { + return; + } + + const line = repl.line.trim(); + + // Do not preview if the command is buffered or if the line is empty. + if (repl[bufferSymbol] || line === '') { + return; + } + + getPreviewInput(line, (error, inspected) => { + // Ignore the output if the value is identical to the current line and the + // former preview is not identical to this preview. + if ((line === inspected && lastPreview !== inspected) || + inspected === null) { + return; + } + if (error) { + debug('Error while generating preview', error); + return; + } + // Do not preview `undefined` if colors are deactivated or explicitly + // requested. + if (inspected === 'undefined' && + (!repl.useColors || repl.ignoreUndefined)) { + return; + } + + preview = inspected; + + // Limit the output to maximum 250 characters. Otherwise it becomes a) + // difficult to read and b) non terminal REPLs would visualize the whole + // output. + const maxColumns = MathMin(repl.columns, 250); + + if (inspected.length > maxColumns) { + inspected = `${inspected.slice(0, maxColumns - 6)}...`; + } + const lineBreakPos = inspected.indexOf('\n'); + if (lineBreakPos !== -1) { + inspected = `${inspected.slice(0, lineBreakPos)}`; + } + const result = repl.useColors ? + `\u001b[90m${inspected}\u001b[39m` : + `// ${inspected}`; + + repl.output.write(`\n${result}`); + moveCursor(repl.output, 0, -1); + cursorTo(repl.output, repl.cursor + repl._prompt.length); + }); + }; + + // Refresh prints the whole screen again and the preview will be removed + // during that procedure. Print the preview again. This also makes sure + // the preview is always correct after resizing the terminal window. + const tmpRefresh = repl._refreshLine.bind(repl); + repl._refreshLine = () => { + preview = null; + tmpRefresh(); + showInputPreview(); + }; + + return { showInputPreview, clearPreview }; +} + module.exports = { isRecoverableError, - kStandaloneREPL: Symbol('kStandaloneREPL') + kStandaloneREPL: Symbol('kStandaloneREPL'), + setupPreview }; diff --git a/lib/repl.js b/lib/repl.js index b6876bb8c6..98b0d2415d 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -98,7 +98,8 @@ const experimentalREPLAwait = require('internal/options').getOptionValue( ); const { isRecoverableError, - kStandaloneREPL + kStandaloneREPL, + setupPreview, } = require('internal/repl/utils'); const { getOwnNonIndexProperties, @@ -204,6 +205,9 @@ function REPLServer(prompt, } } + const preview = options.terminal && + (options.preview !== undefined ? !!options.preview : true); + this.inputStream = options.input; this.outputStream = options.output; this.useColors = !!options.useColors; @@ -804,9 +808,20 @@ function REPLServer(prompt, } }); + const { + clearPreview, + showInputPreview + } = setupPreview( + this, + kContextId, + kBufferedCommandSymbol, + preview + ); + // Wrap readline tty to enable editor mode and pausing. const ttyWrite = self._ttyWrite.bind(self); self._ttyWrite = (d, key) => { + clearPreview(); key = key || {}; if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === 'c')) { pausedBuffer.push(['key', [d, key]]); @@ -819,6 +834,7 @@ function REPLServer(prompt, self.clearLine(); } ttyWrite(d, key); + showInputPreview(); return; } diff --git a/test/parallel/test-repl-history-navigation.js b/test/parallel/test-repl-history-navigation.js index b00f932aa9..3bd198880f 100644 --- a/test/parallel/test-repl-history-navigation.js +++ b/test/parallel/test-repl-history-navigation.js @@ -46,28 +46,50 @@ ActionStream.prototype.readable = true; const ENTER = { name: 'enter' }; const UP = { name: 'up' }; const DOWN = { name: 'down' }; +const LEFT = { name: 'left' }; +const DELETE = { name: 'delete' }; const prompt = '> '; +const prev = process.features.inspector; + const tests = [ { // Creates few history to navigate for env: { NODE_REPL_HISTORY: defaultHistoryPath }, test: [ 'let ab = 45', ENTER, '555 + 909', ENTER, - '{key : {key2 :[] }}', ENTER], + '{key : {key2 :[] }}', ENTER, + 'Array(100).fill(1).map((e, i) => i ** i)', LEFT, LEFT, DELETE, + '2', ENTER], expected: [], clean: false }, { env: { NODE_REPL_HISTORY: defaultHistoryPath }, - test: [UP, UP, UP, UP, DOWN, DOWN, DOWN], + test: [UP, UP, UP, UP, UP, DOWN, DOWN, DOWN, DOWN], expected: [prompt, + `${prompt}Array(100).fill(1).map((e, i) => i ** 2)`, + prev && '\n// [ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, ' + + '144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529,' + + ' 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, ' + + '1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936,' + + ' 2025, 2116, 2209, ...', `${prompt}{key : {key2 :[] }}`, + prev && '\n// { key: { key2: [] } }', `${prompt}555 + 909`, + prev && '\n// 1464', `${prompt}let ab = 45`, `${prompt}555 + 909`, + prev && '\n// 1464', `${prompt}{key : {key2 :[] }}`, - prompt], + prev && '\n// { key: { key2: [] } }', + `${prompt}Array(100).fill(1).map((e, i) => i ** 2)`, + prev && '\n// [ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, ' + + '144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529,' + + ' 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, ' + + '1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936,' + + ' 2025, 2116, 2209, ...', + prompt].filter((e) => typeof e === 'string'), clean: true } ]; diff --git a/test/parallel/test-repl-multiline.js b/test/parallel/test-repl-multiline.js index 454d5b1019..6498923b62 100644 --- a/test/parallel/test-repl-multiline.js +++ b/test/parallel/test-repl-multiline.js @@ -3,34 +3,44 @@ const common = require('../common'); const ArrayStream = require('../common/arraystream'); const assert = require('assert'); const repl = require('repl'); -const inputStream = new ArrayStream(); -const outputStream = new ArrayStream(); -const input = ['const foo = {', '};', 'foo;']; -let output = ''; +const input = ['const foo = {', '};', 'foo']; -outputStream.write = (data) => { output += data.replace('\r', ''); }; +function run({ useColors }) { + const inputStream = new ArrayStream(); + const outputStream = new ArrayStream(); + let output = ''; -const r = repl.start({ - prompt: '', - input: inputStream, - output: outputStream, - terminal: true, - useColors: false -}); + outputStream.write = (data) => { output += data.replace('\r', ''); }; -r.on('exit', common.mustCall(() => { - const actual = output.split('\n'); + const r = repl.start({ + prompt: '', + input: inputStream, + output: outputStream, + terminal: true, + useColors + }); - // Validate the output, which contains terminal escape codes. - assert.strictEqual(actual.length, 6); - assert.ok(actual[0].endsWith(input[0])); - assert.ok(actual[1].includes('... ')); - assert.ok(actual[1].endsWith(input[1])); - assert.strictEqual(actual[2], 'undefined'); - assert.ok(actual[3].endsWith(input[2])); - assert.strictEqual(actual[4], '{}'); - // Ignore the last line, which is nothing but escape codes. -})); + r.on('exit', common.mustCall(() => { + const actual = output.split('\n'); -inputStream.run(input); -r.close(); + // Validate the output, which contains terminal escape codes. + assert.strictEqual(actual.length, 6 + process.features.inspector); + assert.ok(actual[0].endsWith(input[0])); + assert.ok(actual[1].includes('... ')); + assert.ok(actual[1].endsWith(input[1])); + assert.ok(actual[2].includes('undefined')); + assert.ok(actual[3].endsWith(input[2])); + if (process.features.inspector) { + assert.ok(actual[4].includes(actual[5])); + assert.strictEqual(actual[4].includes('//'), !useColors); + } + assert.strictEqual(actual[4 + process.features.inspector], '{}'); + // Ignore the last line, which is nothing but escape codes. + })); + + inputStream.run(input); + r.close(); +} + +run({ useColors: true }); +run({ useColors: false }); diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js new file mode 100644 index 0000000000..92e73dd245 --- /dev/null +++ b/test/parallel/test-repl-preview.js @@ -0,0 +1,131 @@ +'use strict'; + +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); +const Repl = require('repl'); + +common.skipIfInspectorDisabled(); + +const PROMPT = 'repl > '; + +class REPLStream extends ArrayStream { + constructor() { + super(); + this.lines = ['']; + } + write(chunk) { + const chunkLines = chunk.toString('utf8').split('\n'); + this.lines[this.lines.length - 1] += chunkLines[0]; + if (chunkLines.length > 1) { + this.lines.push(...chunkLines.slice(1)); + } + this.emit('line'); + return true; + } + wait() { + this.lines = ['']; + return new Promise((resolve, reject) => { + const onError = (err) => { + this.removeListener('line', onLine); + reject(err); + }; + const onLine = () => { + if (this.lines[this.lines.length - 1].includes(PROMPT)) { + this.removeListener('error', onError); + this.removeListener('line', onLine); + resolve(this.lines); + } + }; + this.once('error', onError); + this.on('line', onLine); + }); + } +} + +function runAndWait(cmds, repl) { + const promise = repl.inputStream.wait(); + for (const cmd of cmds) { + repl.inputStream.run([cmd]); + } + return promise; +} + +async function tests(options) { + const repl = Repl.start({ + prompt: PROMPT, + stream: new REPLStream(), + ignoreUndefined: true, + useColors: true, + ...options + }); + + repl.inputStream.run([ + 'function foo(x) { return x; }', + 'function koo() { console.log("abc"); }', + 'a = undefined;' + ]); + const testCases = [ + ['foo', [2, 4], '[Function: foo]', + 'foo', + '\x1B[90m[Function: foo]\x1B[39m\x1B[1A\x1B[11G\x1B[1B\x1B[2K\x1B[1A\r', + '\x1B[36m[Function: foo]\x1B[39m', + '\x1B[1G\x1B[0Jrepl > \x1B[8G'], + ['koo', [2, 4], '[Function: koo]', + 'koo', + '\x1B[90m[Function: koo]\x1B[39m\x1B[1A\x1B[11G\x1B[1B\x1B[2K\x1B[1A\r', + '\x1B[36m[Function: koo]\x1B[39m', + '\x1B[1G\x1B[0Jrepl > \x1B[8G'], + ['a', [1, 2], undefined], + ['{ a: true }', [2, 3], '{ a: \x1B[33mtrue\x1B[39m }', + '{ a: true }\r', + '{ a: \x1B[33mtrue\x1B[39m }', + '\x1B[1G\x1B[0Jrepl > \x1B[8G'], + ['1n + 2n', [2, 5], '\x1B[33m3n\x1B[39m', + '1n + 2', + '\x1B[90mType[39m\x1B[1A\x1B[14G\x1B[1B\x1B[2K\x1B[1An', + '\x1B[90m3n\x1B[39m\x1B[1A\x1B[15G\x1B[1B\x1B[2K\x1B[1A\r', + '\x1B[33m3n\x1B[39m', + '\x1B[1G\x1B[0Jrepl > \x1B[8G'], + ['{ a: true };', [2, 4], '\x1B[33mtrue\x1B[39m', + '{ a: true };', + '\x1B[90mtrue\x1B[39m\x1B[1A\x1B[20G\x1B[1B\x1B[2K\x1B[1A\r', + '\x1B[33mtrue\x1B[39m', + '\x1B[1G\x1B[0Jrepl > \x1B[8G'], + [' \t { a: true};', [2, 5], '\x1B[33mtrue\x1B[39m', + ' \t { a: true}', + '\x1B[90m{ a: true }\x1B[39m\x1B[1A\x1B[21G\x1B[1B\x1B[2K\x1B[1A;', + '\x1B[90mtrue\x1B[39m\x1B[1A\x1B[22G\x1B[1B\x1B[2K\x1B[1A\r', + '\x1B[33mtrue\x1B[39m', + '\x1B[1G\x1B[0Jrepl > \x1B[8G'] + ]; + + const hasPreview = repl.terminal && + (options.preview !== undefined ? !!options.preview : true); + + for (const [input, length, expected, ...preview] of testCases) { + console.log(`Testing ${input}`); + + const toBeRun = input.split('\n'); + let lines = await runAndWait(toBeRun, repl); + + assert.strictEqual(lines.length, length[+hasPreview]); + if (expected === undefined) { + assert(!lines.some((e) => e.includes('undefined'))); + } else if (hasPreview) { + // Remove error messages. That allows the code to run in different + // engines. + // eslint-disable-next-line no-control-regex + lines = lines.map((line) => line.replace(/Error: .+?\x1B/, '')); + assert.deepStrictEqual(lines, preview); + } else { + assert.ok(lines[0].includes(expected), lines); + } + } +} + +tests({ terminal: false }); // No preview +tests({ terminal: true }); // Preview +tests({ terminal: false, preview: false }); // No preview +tests({ terminal: false, preview: true }); // No preview +tests({ terminal: true, preview: true }); // Preview diff --git a/test/parallel/test-repl-top-level-await.js b/test/parallel/test-repl-top-level-await.js index 5c8aa95d40..cecbd3ab45 100644 --- a/test/parallel/test-repl-top-level-await.js +++ b/test/parallel/test-repl-top-level-await.js @@ -1,11 +1,13 @@ 'use strict'; -require('../common'); +const common = require('../common'); const ArrayStream = require('../common/arraystream'); const assert = require('assert'); const { stripVTControlCharacters } = require('internal/readline/utils'); const repl = require('repl'); +common.skipIfInspectorDisabled(); + // Flags: --expose-internals --experimental-repl-await const PROMPT = 'await repl > '; @@ -84,61 +86,84 @@ async function ordinaryTests() { const testCases = [ [ 'await Promise.resolve(0)', '0' ], [ '{ a: await Promise.resolve(1) }', '{ a: 1 }' ], - [ '_', '{ a: 1 }' ], - [ 'let { a, b } = await Promise.resolve({ a: 1, b: 2 }), f = 5;', + [ '_', '// { a: 1 }\r', { line: 0 } ], + [ 'let { aa, bb } = await Promise.resolve({ aa: 1, bb: 2 }), f = 5;', 'undefined' ], - [ 'a', '1' ], - [ 'b', '2' ], - [ 'f', '5' ], - [ 'let c = await Promise.resolve(2)', 'undefined' ], - [ 'c', '2' ], - [ 'let d;', 'undefined' ], - [ 'd', 'undefined' ], - [ 'let [i, { abc: { k } }] = [0, { abc: { k: 1 } }];', 'undefined' ], - [ 'i', '0' ], - [ 'k', '1' ], - [ 'var l = await Promise.resolve(2);', 'undefined' ], - [ 'l', '2' ], - [ 'foo(await koo())', '4' ], - [ '_', '4' ], - [ 'const m = foo(await koo());', 'undefined' ], - [ 'm', '4' ], - [ 'const n = foo(await\nkoo());', 'undefined' ], - [ 'n', '4' ], + [ 'aa', ['// 1\r', '1'] ], + [ 'bb', ['// 2\r', '2'] ], + [ 'f', ['// 5\r', '5'] ], + [ 'let cc = await Promise.resolve(2)', 'undefined' ], + [ 'cc', ['// 2\r', '2'] ], + [ 'let dd;', 'undefined' ], + [ 'dd', 'undefined' ], + [ 'let [ii, { abc: { kk } }] = [0, { abc: { kk: 1 } }];', 'undefined' ], + [ 'ii', ['// 0\r', '0'] ], + [ 'kk', ['// 1\r', '1'] ], + [ 'var ll = await Promise.resolve(2);', 'undefined' ], + [ 'll', ['// 2\r', '2'] ], + [ 'foo(await koo())', + [ 'f', '// 5oo', '// [Function: foo](await koo())\r', '4' ] ], + [ '_', ['// 4\r', '4'] ], + [ 'const m = foo(await koo());', + [ 'const m = foo(await koo());\r', 'undefined' ] ], + [ 'm', ['// 4\r', '4' ] ], + [ 'const n = foo(await\nkoo());', + [ 'const n = foo(await\r', '... koo());\r', 'undefined' ] ], + [ 'n', ['// 4\r', '4' ] ], // eslint-disable-next-line no-template-curly-in-string [ '`status: ${(await Promise.resolve({ status: 200 })).status}`', "'status: 200'"], - [ 'for (let i = 0; i < 2; ++i) await i', 'undefined' ], - [ 'for (let i = 0; i < 2; ++i) { await i }', 'undefined' ], - [ 'await 0', '0' ], - [ 'await 0; function foo() {}', 'undefined' ], - [ 'foo', '[Function: foo]' ], - [ 'class Foo {}; await 1;', '1' ], - [ 'Foo', '[Function: Foo]' ], - [ 'if (await true) { function bar() {}; }', 'undefined' ], - [ 'bar', '[Function: bar]' ], + [ 'for (let i = 0; i < 2; ++i) await i', + ['f', '// 5or (let i = 0; i < 2; ++i) await i\r', 'undefined'] ], + [ 'for (let i = 0; i < 2; ++i) { await i }', + [ 'f', '// 5or (let i = 0; i < 2; ++i) { await i }\r', 'undefined' ] ], + [ 'await 0', ['await 0\r', '0'] ], + [ 'await 0; function foo() {}', + ['await 0; function foo() {}\r', 'undefined'] ], + [ 'foo', + ['f', '// 5oo', '// [Function: foo]\r', '[Function: foo]'] ], + [ 'class Foo {}; await 1;', ['class Foo {}; await 1;\r', '1'] ], + [ 'Foo', ['// [Function: Foo]\r', '[Function: Foo]'] ], + [ 'if (await true) { function bar() {}; }', + ['if (await true) { function bar() {}; }\r', 'undefined'] ], + [ 'bar', ['// [Function: bar]\r', '[Function: bar]'] ], [ 'if (await true) { class Bar {}; }', 'undefined' ], [ 'Bar', 'Uncaught ReferenceError: Bar is not defined' ], [ 'await 0; function* gen(){}', 'undefined' ], - [ 'for (var i = 0; i < 10; ++i) { await i; }', 'undefined' ], - [ 'i', '10' ], - [ 'for (let j = 0; j < 5; ++j) { await j; }', 'undefined' ], - [ 'j', 'Uncaught ReferenceError: j is not defined' ], - [ 'gen', '[GeneratorFunction: gen]' ], + [ 'for (var i = 0; i < 10; ++i) { await i; }', + ['f', '// 5or (var i = 0; i < 10; ++i) { await i; }\r', 'undefined'] ], + [ 'i', ['// 10\r', '10'] ], + [ 'for (let j = 0; j < 5; ++j) { await j; }', + ['f', '// 5or (let j = 0; j < 5; ++j) { await j; }\r', 'undefined'] ], + [ 'j', 'Uncaught ReferenceError: j is not defined', { line: 0 } ], + [ 'gen', ['// [GeneratorFunction: gen]\r', '[GeneratorFunction: gen]'] ], [ 'return 42; await 5;', 'Uncaught SyntaxError: Illegal return statement', { line: 3 } ], [ 'let o = await 1, p', 'undefined' ], [ 'p', 'undefined' ], [ 'let q = 1, s = await 2', 'undefined' ], - [ 's', '2' ], - [ 'for await (let i of [1,2,3]) console.log(i)', 'undefined', { line: 3 } ] + [ 's', ['// 2\r', '2'] ], + [ 'for await (let i of [1,2,3]) console.log(i)', + [ + 'f', + '// 5or await (let i of [1,2,3]) console.log(i)\r', + '1', + '2', + '3', + 'undefined' + ] + ] ]; for (const [input, expected, options = {}] of testCases) { console.log(`Testing ${input}`); const toBeRun = input.split('\n'); const lines = await runAndWait(toBeRun); - if ('line' in options) { + if (Array.isArray(expected)) { + if (lines[0] === input) + lines.shift(); + assert.deepStrictEqual(lines, [...expected, PROMPT]); + } else if ('line' in options) { assert.strictEqual(lines[toBeRun.length + options.line], expected); } else { const echoed = toBeRun.map((a, i) => `${i > 0 ? '... ' : ''}${a}\r`); |