Skip to content

Commit 9413135

Browse files
test: add tests for REPL custom evals
this commit reintroduces the REPL custom eval tests that have been introduced in #57691 but reverted in #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 can resolve with a small delay
1 parent 95b0f9d commit 9413135

File tree

3 files changed

+164
-34
lines changed

3 files changed

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

test/parallel/test-repl-eval.js

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

0 commit comments

Comments
 (0)