Repository: nodejs/node
Pull request: test: add tests for REPL custom evals

I noticed this TODO comment mentioning that some tests were needed for custom REPL evals so I figured I could add some :slightly_smiling_face: 

I had a double check and I don't think that the functionality I am testing here is already tested in other test files :slightly_smiling_face: 
<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/repl.js
patch: @@ -302,7 +302,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_);
 

filename: test/parallel/test-repl-custom-eval.js
patch: @@ -0,0 +1,135 @@
+'use strict';
+
+require('../common');
+const ArrayStream = require('../common/arraystream');
+const assert = require('assert');
+const { describe, it } = require('node:test');
+
+const repl = require('repl');
+
+function getReplOutput(input, replOptions, run = true) {
+  const inputStream = new ArrayStream();
+  const outputStream = new ArrayStream();
+
+  repl.start({
+    input: inputStream,
+    output: outputStream,
+    ...replOptions,
+  });
+
+  let output = '';
+  outputStream.write = (chunk) => (output += chunk);
+
+  inputStream.emit('data', input);
+
+  if (run) {
+    inputStream.run(['']);
+  }
+
+  return output;
+}
+
+describe('repl with custom eval', { concurrency: true }, () => {
+  it('uses the custom eval function as expected', () => {
+    const output = getReplOutput('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', () => {
+    const output = getReplOutput('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');
+  });
+
+  it("doesn't show previews by default", () => {
+    const input = "'Hello custom' + ' eval World!'";
+    const output = getReplOutput(input, {
+      terminal: true,
+      eval: (code, _ctx, _replRes, cb) => cb(null, eval(code)),
+    }, false);
+    assert.strictEqual(output, input);
+    assert.doesNotMatch(output, /Hello custom eval World!/);
+  });
+
+  it('does show previews if `preview` is set to `true`', () => {
+    const input = "'Hello custom' + ' eval World!'";
+    const output = getReplOutput(input, {
+      terminal: true,
+      eval: (code, _ctx, _replRes, cb) => cb(null, eval(code)),
+      preview: true,
+    }, false);
+
+    const escapedInput = input.replace(/\+/g, '\\+'); // TODO: migrate to `RegExp.escape` when it's available.
+    assert.match(
+      output,
+      new RegExp(`${escapedInput}\n// 'Hello custom eval World!'`)
+    );
+  });
+});

filename: test/parallel/test-repl-eval.js
patch: @@ -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);
-}