Skip to content

Commit 584b6d9

Browse files
test: add tests for REPL custom evals
this commit reintroduces the REPL custom eval tests that have been introduced in nodejs#57691 but reverted in nodejs#57793 the tests turned out problematic before because `getReplOutput`, the function used to return the repl output, wasn't taking into account that input processing and output emitting are asynchronous operation can resolve with a small delay
1 parent 95b0f9d commit 584b6d9

File tree

3 files changed

+160
-34
lines changed

3 files changed

+160
-34
lines changed

lib/repl.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,6 @@ function REPLServer(prompt,
302302
options.useColors = shouldColorize(options.output);
303303
}
304304

305-
// TODO(devsnek): Add a test case for custom eval functions.
306305
const preview = options.terminal &&
307306
(options.preview !== undefined ? !!options.preview : !eval_);
308307

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
'use strict';
2+
3+
require('../common');
4+
const ArrayStream = require('../common/arraystream');
5+
const assert = require('assert');
6+
const { describe, it } = require('node:test');
7+
8+
const repl = require('repl');
9+
10+
// Processes some input in a REPL instance and resolves to the repl's
11+
// output when the provided exitCondition is met, or after a certain
12+
// timeout threshold if the condition is never met
13+
function getReplOutput(input, exitCondition, replOptions, run = true) {
14+
return new Promise((resolve) => {
15+
const inputStream = new ArrayStream();
16+
const outputStream = new ArrayStream();
17+
18+
const replServer = repl.start({
19+
input: inputStream,
20+
output: outputStream,
21+
...replOptions,
22+
});
23+
24+
let output = '';
25+
26+
const timeoutTimer = setTimeout(() => {
27+
replServer.close();
28+
resolve(output);
29+
}, 3_000);
30+
31+
outputStream.write = (chunk) => {
32+
output += chunk;
33+
if (exitCondition(output)) {
34+
clearTimeout(timeoutTimer);
35+
replServer.close();
36+
resolve(output);
37+
}
38+
};
39+
40+
inputStream.emit('data', input);
41+
42+
if (run) {
43+
inputStream.run(['']);
44+
}
45+
46+
return output;
47+
});
48+
}
49+
50+
describe('repl with custom eval', { concurrency: true }, () => {
51+
it('uses the custom eval function as expected', async () => {
52+
const regexp = /Convert this to upper case\r\n'CONVERT THIS TO UPPER CASE\\n'/;
53+
const output = await getReplOutput('Convert this to upper case', (output) => regexp.test(output), {
54+
terminal: true,
55+
eval: (code, _ctx, _replRes, cb) => cb(null, code.toUpperCase()),
56+
});
57+
assert.match(output, regexp);
58+
});
59+
60+
it('surfaces errors as expected', async () => {
61+
const regexp = /Uncaught Error: Testing Error\n/;
62+
const output = await getReplOutput('Convert this to upper case',
63+
(output) => regexp.test(output), {
64+
terminal: true,
65+
eval: (_code, _ctx, _replRes, cb) => cb(new Error('Testing Error')),
66+
});
67+
assert.match(output, regexp);
68+
});
69+
70+
it('provides a repl context to the eval callback', async () => {
71+
const context = await new Promise((resolve) => {
72+
const r = repl.start({
73+
eval: (_cmd, context) => resolve(context),
74+
});
75+
r.context = { foo: 'bar' };
76+
r.write('\n.exit\n');
77+
});
78+
assert.strictEqual(context.foo, 'bar');
79+
});
80+
81+
it('provides the global context to the eval callback', async () => {
82+
const context = await new Promise((resolve) => {
83+
const r = repl.start({
84+
useGlobal: true,
85+
eval: (_cmd, context) => resolve(context),
86+
});
87+
global.foo = 'global_foo';
88+
r.write('\n.exit\n');
89+
});
90+
91+
assert.strictEqual(context.foo, 'global_foo');
92+
delete global.foo;
93+
});
94+
95+
it('inherits variables from the global context but does not use it afterwords if `useGlobal` is false', async () => {
96+
global.bar = 'global_bar';
97+
const context = await new Promise((resolve) => {
98+
const r = repl.start({
99+
useGlobal: false,
100+
eval: (_cmd, context) => resolve(context),
101+
});
102+
global.baz = 'global_baz';
103+
r.write('\n.exit\n');
104+
});
105+
106+
assert.strictEqual(context.bar, 'global_bar');
107+
assert.notStrictEqual(context.baz, 'global_baz');
108+
delete global.bar;
109+
delete global.baz;
110+
});
111+
112+
/**
113+
* Default preprocessor transforms
114+
* function f() {} to
115+
* var f = function f() {}
116+
* This test ensures that original input is preserved.
117+
* Reference: https://github.com/nodejs/node/issues/9743
118+
*/
119+
it('preserves the original input', async () => {
120+
const cmd = await new Promise((resolve) => {
121+
const r = repl.start({
122+
eval: (cmd) => resolve(cmd),
123+
});
124+
r.write('function f() {}\n.exit\n');
125+
});
126+
assert.strictEqual(cmd, 'function f() {}\n');
127+
});
128+
129+
it("doesn't show previews by default", async () => {
130+
const input = "'Hello custom' + ' eval World!'";
131+
const output = await getReplOutput(
132+
input,
133+
() => false,
134+
{
135+
terminal: true,
136+
eval: (code, _ctx, _replRes, cb) => cb(null, eval(code)),
137+
},
138+
false
139+
);
140+
assert.strictEqual(output, input);
141+
assert.doesNotMatch(output, /Hello custom eval World!/);
142+
});
143+
144+
it('does show previews if `preview` is set to `true`', async () => {
145+
const input = "'Hello custom' + ' eval World!'";
146+
const escapedInput = input.replace(/\+/g, '\\+'); // TODO: migrate to `RegExp.escape` when it's available.
147+
const regexp = new RegExp(`${escapedInput}\n// 'Hello custom eval World!'`);
148+
const output = await getReplOutput(
149+
input,
150+
(output) => regexp.test(output),
151+
{
152+
terminal: true,
153+
eval: (code, _ctx, _replRes, cb) => cb(null, eval(code)),
154+
preview: true,
155+
},
156+
false
157+
);
158+
assert.match(output, regexp);
159+
});
160+
});

test/parallel/test-repl-eval.js

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)