Files
node/test/parallel/test-repl-preview.mjs
T
2026-07-25 23:56:46 -07:00

306 lines
9.4 KiB
JavaScript

import * as common from '../common/index.mjs';
import assert from 'node:assert';
import events from 'node:events';
import { REPLServer } from 'node:repl';
import { Stream } from 'node:stream';
import { inspect } from 'node:util';
common.skipIfInspectorDisabled();
// Ignore terminal settings. This is so the test can be run intact if TERM=dumb.
process.env.TERM = '';
const PROMPT = 'repl > ';
class REPLStream extends Stream {
readable = true;
writable = true;
constructor() {
super();
this.lines = [''];
this.writeCount = 0;
}
async run(data) {
for (const entry of data) {
this.emit('data', entry);
await this.settle();
}
this.emit('data', '\n');
}
settle() {
return new Promise((resolve) => {
let last = this.writeCount;
let quiet = 0;
const check = () => {
if (this.writeCount !== last) {
last = this.writeCount;
quiet = 0;
} else if (++quiet >= 10) {
resolve();
return;
}
setImmediate(check);
};
setImmediate(check);
});
}
write(chunk) {
this.writeCount++;
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', this.lines[this.lines.length - 1]);
return true;
}
async wait() {
this.lines = [''];
for await (const [line] of events.on(this, 'line')) {
if (line.includes(PROMPT)) {
return this.lines;
}
}
}
pause() {}
resume() {}
}
async function runAndWait(cmds, repl) {
const promise = repl.inputStream.wait();
for (const cmd of cmds) {
await repl.inputStream.run(cmd);
}
return promise;
}
function stripStackFrames(lines) {
return lines.filter((line) => !/^\s*at /.test(line));
}
async function tests(options) {
const repl = new REPLServer({
prompt: PROMPT,
stream: new REPLStream(),
ignoreUndefined: true,
// Keep syntax highlighting out of these preview transcript assertions.
// Preview and result colors are enabled after readline is initialized.
useColors: false,
...options
});
repl.useColors = true;
repl.writer.options.colors = true;
await runAndWait([
'function foo(x) { return x; } ' +
'function koo() { console.log("abc"); } ' +
'a = undefined;',
], repl);
const testCases = [{
input: 'foo',
noPreview: '[Function: foo]',
preview: [
'foo',
'\x1B[90m[Function: foo]\x1B[39m\x1B[11G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'\x1B[36m[Function: foo]\x1B[39m',
]
}, {
input: 'koo',
noPreview: '[Function: koo]',
preview: [
'k\x1B[90moo\x1B[39m\x1B[9G',
'\x1B[90m[Function: koo]\x1B[39m\x1B[9G\x1B[1A\x1B[1B\x1B[2K\x1B[1A' +
'\x1B[0Ko\x1B[90mo\x1B[39m\x1B[10G',
'\x1B[90m[Function: koo]\x1B[39m\x1B[10G\x1B[1A\x1B[1B\x1B[2K\x1B[1A' +
'\x1B[0Ko',
'\x1B[90m[Function: koo]\x1B[39m\x1B[11G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'\x1B[36m[Function: koo]\x1B[39m',
]
}, {
input: 'a',
noPreview: 'repl > ', // No "undefined" output.
preview: ['a\r'] // No "undefined" preview.
}, {
input: " { b: 1 }['b'] === 1",
noPreview: '\x1B[33mtrue\x1B[39m',
preview: [
" { b: 1 }['b']",
'\x1B[90m1\x1B[39m\x1B[22G\x1B[1A\x1B[1B\x1B[2K\x1B[1A ',
'\x1B[90m1\x1B[39m\x1B[23G\x1B[1A\x1B[1B\x1B[2K\x1B[1A=== 1',
'\x1B[90mtrue\x1B[39m\x1B[28G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'\x1B[33mtrue\x1B[39m',
]
}, {
input: "{ b: 1 }['b'] === 1;",
noPreview: '\x1B[33mfalse\x1B[39m',
preview: [
"{ b: 1 }['b']",
'\x1B[90m1\x1B[39m\x1B[21G\x1B[1A\x1B[1B\x1B[2K\x1B[1A ',
'\x1B[90m1\x1B[39m\x1B[22G\x1B[1A\x1B[1B\x1B[2K\x1B[1A=== 1',
'\x1B[90mtrue\x1B[39m\x1B[27G\x1B[1A\x1B[1B\x1B[2K\x1B[1A;',
'\x1B[90mfalse\x1B[39m\x1B[28G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'\x1B[33mfalse\x1B[39m',
]
}, {
input: '{ a: true }',
noPreview: '{ a: \x1B[33mtrue\x1B[39m }',
preview: [
'{ a: tru\x1B[90me\x1B[39m\x1B[16G\x1B[0Ke }\r',
'{ a: \x1B[33mtrue\x1B[39m }',
]
}, {
input: '{ a: true };',
noPreview: '\x1B[33mtrue\x1B[39m',
preview: [
'{ a: tru\x1B[90me\x1B[39m\x1B[16G\x1B[0Ke };',
'\x1B[90mtrue\x1B[39m\x1B[20G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'\x1B[33mtrue\x1B[39m',
]
}, {
input: ' \t { a: true};',
noPreview: '\x1B[33mtrue\x1B[39m',
preview: [
' { a: tru\x1B[90me\x1B[39m\x1B[18G\x1B[0Ke}',
'\x1B[90m{ a: true }\x1B[39m\x1B[20G\x1B[1A\x1B[1B\x1B[2K\x1B[1A;',
'\x1B[90mtrue\x1B[39m\x1B[21G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'\x1B[33mtrue\x1B[39m',
]
}, {
input: '1n + 2n',
noPreview: '\x1B[33m3n\x1B[39m',
preview: [
'1n + 2',
'\x1B[90mType[39m\x1B[14G\x1B[1A\x1B[1B\x1B[2K\x1B[1An',
'\x1B[90m3n\x1B[39m\x1B[15G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'\x1B[33m3n\x1B[39m',
]
}, {
// The inspector evaluator treats `{};1` as a block statement followed by a
// labelled/expression statement and rejects the trailing expression. The
// eager-eval preview still shows the result of the wrapped expression (`1`),
// but the committed evaluation reports a SyntaxError.
input: '{};1',
noPreview: 'Uncaught SyntaxError: Unexpected token \';\'',
preview: [
'{};1',
'\x1B[90m1\x1B[39m\x1B[12G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'Uncaught SyntaxError: Unexpected token \';\'',
]
}, {
input: 'aaaa',
noPreview: 'Uncaught ReferenceError: aaaa is not defined',
preview: [
'aaaa\r',
'Uncaught ReferenceError: aaaa is not defined',
]
}, {
input: '/0',
noPreview: 'Uncaught SyntaxError: Invalid regular expression: missing /',
preview: [
'/0\r',
'Uncaught SyntaxError: Invalid regular expression: missing /',
]
}, {
input: '{})',
noPreview: "Uncaught SyntaxError: Unexpected token ')'",
preview: [
'{})\r',
"Uncaught SyntaxError: Unexpected token ')'",
],
}, {
input: "{ a: '{' }",
noPreview: "{ a: \x1B[32m'{'\x1B[39m }",
preview: [
"{ a: '{' }\r",
"{ a: \x1B[32m'{'\x1B[39m }",
],
}, {
input: "{'{':0}",
noPreview: "{ \x1B[32m'{'\x1B[39m: \x1B[33m0\x1B[39m }",
preview: [
"{'{':0}",
"\x1B[90m{ '{': 0 }\x1B[39m\x1B[15G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r",
"{ \x1B[32m'{'\x1B[39m: \x1B[33m0\x1B[39m }",
],
}, {
input: '{[Symbol.for("{")]: 0 }',
noPreview: '{ \x1B[32mSymbol({)\x1B[39m: \x1B[33m0\x1B[39m }',
preview: [
'{[Sym\x1B[90mbol\x1B[39m\x1B[13G\x1B[0Kb\x1B[90mol\x1B[39m\x1B[14G\x1B[0Ko' +
'\x1B[90ml\x1B[39m\x1B[15G\x1B[0Kl.f\x1B[90mor\x1B[39m\x1B[18G\x1B[0Ko' +
'\x1B[90mr\x1B[39m\x1B[19G\x1B[0Kr("{")]: 0 }\r',
'{ \x1B[32mSymbol({)\x1B[39m: \x1B[33m0\x1B[39m }',
],
}, {
input: '{},{}',
noPreview: '{}',
preview: [
'{},{}',
'\x1B[90m{}\x1B[39m\x1B[13G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'{}',
],
}, {
input: '{} //;',
noPreview: 'repl > ',
preview: [
'{} //;\r',
],
}, {
// The inspector evaluator wraps the input in parentheses to detect an
// expression, which turns `{throw 0}` into invalid syntax. The eager-eval
// preview still shows the thrown value, but the committed evaluation reports
// a SyntaxError.
input: '{throw 0}',
noPreview: 'Uncaught SyntaxError: Unexpected number',
preview: [
// Typing `{thr` offers the `throw` keyword as an inline completion preview.
'{thr\x1B[90mow\x1B[39m\x1B[12G\x1B[0Ko\x1B[90mw\x1B[39m\x1B[13G\x1B[0Kw 0}',
'\x1B[90m0\x1B[39m\x1B[17G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
'Uncaught SyntaxError: Unexpected number',
],
}];
const hasPreview = repl.terminal &&
(options.preview !== undefined ? !!options.preview : true);
for (const { input, noPreview, preview } of testCases) {
console.log(`Testing ${input}`);
const toBeRun = input.split('\n');
let lines = await runAndWait(toBeRun, repl);
// Drop unstable inspector stack frames (see `stripStackFrames`).
lines = stripStackFrames(lines);
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.strictEqual(lines.pop(), '\x1B[1G\x1B[0Jrepl > \x1B[8G');
assert.deepStrictEqual(lines, preview);
} else {
assert.ok(lines[0].includes(noPreview), lines.map(inspect).join('\n'));
if (preview.length !== 1 || preview[0] !== `${input}\r`) {
assert.strictEqual(lines.length, 2);
}
}
}
repl.close();
}
// NOTE: The input `{} //` (a block followed by a line comment, with no trailing
// semicolon) is intentionally NOT exercised here. Under the inspector-based
// evaluator it is treated as an incomplete statement: the REPL emits the
// continuation prompt (`| `) and waits for more input forever instead of
// completing. This is a regression from the previous vm-based REPL, where the
// same input evaluated to `undefined` and completed. See the test report.
await tests({ terminal: false }); // No preview
await tests({ terminal: true }); // Preview
await tests({ terminal: false, preview: false }); // No preview
await tests({ terminal: false, preview: true }); // No preview
await tests({ terminal: true, preview: true }); // Preview