Repository: nodejs/node
Pull request: test_runner: allow special characters in snapshot keys

Use `JSONStringify` to serialise snapshot keys to allow special characters such as `\r` to be used in test names

Fixes: https://github.com/nodejs/node/issues/56836

<!--
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/internal/test_runner/snapshot.js
patch: @@ -77,7 +77,7 @@ class SnapshotFile {
   }
 
   setSnapshot(id, value) {
-    this.snapshots[templateEscape(id)] = value;
+    this.snapshots[escapeSnapshotKey(id)] = value;
   }
 
   nextId(name) {
@@ -290,6 +290,13 @@ function validateFunctionArray(fns, name) {
   }
 }
 
+function escapeSnapshotKey(str) {
+  let result = JSONStringify(str, null, 2).slice(1, -1);
+  result = StringPrototypeReplaceAll(result, '`', '\\`');
+  result = StringPrototypeReplaceAll(result, '${', '\\${');
+  return result;
+}
+
 function templateEscape(str) {
   let result = String(str);
   result = StringPrototypeReplaceAll(result, '\\', '\\\\');

filename: test/fixtures/test-runner/snapshots/special-character.js
patch: @@ -0,0 +1,19 @@
+'use strict';
+const { snapshot, test } = require('node:test');
+const { basename, join } = require('node:path');
+
+snapshot.setResolveSnapshotPath((testFile) => {
+  return join(process.cwd(), `${basename(testFile)}.snapshot`);
+});
+
+test('\r', (t) => {
+  t.assert.snapshot({ key: 'value' });
+});
+
+test(String.fromCharCode(55296), (t) => {
+  t.assert.snapshot({ key: 'value' });
+});
+
+test(String.fromCharCode(57343), (t) => {
+  t.assert.snapshot({ key: 'value' });
+});

filename: test/parallel/test-runner-snapshot-tests.js
patch: @@ -158,6 +158,8 @@ suite('SnapshotManager', () => {
 
     file.setSnapshot('foo`${x}` 1', 'test');
     t.assert.strictEqual(file.getSnapshot('foo\\`\\${x}\\` 1'), 'test');
+    file.setSnapshot('\r 1', 'test');
+    t.assert.strictEqual(file.getSnapshot('\\r 1'), 'test');
   });
 
   test('throws if snapshot file cannot be resolved', (t) => {
@@ -340,6 +342,30 @@ test('t.assert.snapshot()', async (t) => {
   });
 });
 
+test('special characters are allowed', async (t) => {
+  const fixture = fixtures.path(
+    'test-runner', 'snapshots', 'special-character.js'
+  );
+
+  await common.spawnPromisified(
+    process.execPath,
+    ['--test-update-snapshots', fixture],
+    { cwd: tmpdir.path },
+  );
+
+  const child = await common.spawnPromisified(
+    process.execPath,
+    [fixture],
+    { cwd: tmpdir.path },
+  );
+
+  t.assert.strictEqual(child.code, 0);
+  t.assert.strictEqual(child.signal, null);
+  t.assert.match(child.stdout, /tests 3/);
+  t.assert.match(child.stdout, /pass 3/);
+  t.assert.match(child.stdout, /fail 0/);
+});
+
 test('snapshots from multiple files (isolation=none)', async (t) => {
   tmpdir.refresh();