Skip to content

Commit aae2d12

Browse files
committed
fix: support node:test snapshot corner-cases
1 parent 860b0d3 commit aae2d12

5 files changed

Lines changed: 598 additions & 9 deletions

File tree

.github/workflows/checks.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
strategy:
3535
fail-fast: false
3636
matrix:
37-
node-version: ['25', '24', '24.0.0', '22.22.0', '22.12.0', '20', '20.19.0']
37+
node-version: ['25', '24', '24.0.0', '22.22.0', '22.12.0', '22.13.0', '20', '20.19.0']
3838
steps:
3939
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
4040
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
@@ -61,7 +61,7 @@ jobs:
6161
strategy:
6262
fail-fast: false
6363
matrix:
64-
node-version: ['25', '24', '24.0.0', '22.22.0', '22.12.0', '20', '20.19.0']
64+
node-version: ['25', '24', '24.0.0', '22.22.0', '22.12.0', '22.13.0', '20', '20.19.0']
6565
steps:
6666
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
6767
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda

src/engine.pure.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
564564

565565
// eslint-disable-next-line no-undef
566566
let snapshotResolver = (dir, name) => [dir, `${name}.snapshot`] // default per Node.js docs
567-
let snapshotSerializers = [(obj) => JSON.stringify(obj, null, 2)]
567+
let snapshotSerializers = [(obj) => (obj === undefined ? `${obj}` : JSON.stringify(obj, null, 2))]
568568
const serializeSnapshot = (obj) => {
569569
let val = obj
570570
for (const fn of snapshotSerializers) val = fn(val)

src/engine.pure.snapshot.cjs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const nameCounts = new Map()
22
let snapshotText, snapshotTextClean
33

44
const escapeSnapshot = (str) => str.replaceAll(/([\\`]|\$\{)/gu, '\\$1')
5+
const escapeSnapshotKey = (s) => escapeSnapshot(s).replaceAll('\n', '\\n').replaceAll('"', '\\"')
56

67
function matchSnapshot(readSnapshot, assert, name, serialized) {
78
// We don't have native snapshots, polyfill reading
@@ -25,12 +26,16 @@ function matchSnapshot(readSnapshot, assert, name, serialized) {
2526
nameCounts.set(name, count)
2627
const escaped = escapeSnapshot(serialized)
2728
const key = `${name} ${count}`
28-
const makeEntry = (x) => `\nexports[\`${escapeSnapshot(key)}\`] = \`${x}\`;\n`
29-
const fixedText = escaped.includes('\r') ? snapshotText : snapshotTextClean // well, if we expect \r let's preserve them
30-
const final = escaped.includes('\n') ? `\n${escaped}\n` : escaped
31-
if (fixedText.includes(makeEntry(final))) return
32-
// Perhaps wrapped with newlines from Node.js snapshots?
33-
if (!final.includes('\n') && fixedText.includes(makeEntry(`\n${final}\n`))) return
29+
// Node.js and jest escape keys differently, both result to same strings, accept both
30+
for (const keyfun of [escapeSnapshot, escapeSnapshotKey]) {
31+
const makeEntry = (x) => `\nexports[\`${keyfun(key)}\`] = \`${x}\`;\n`
32+
const fixedText = escaped.includes('\r') ? snapshotText : snapshotTextClean // well, if we expect \r let's preserve them
33+
const final = escaped.includes('\n') ? `\n${escaped}\n` : escaped
34+
if (fixedText.includes(makeEntry(final))) return
35+
// Perhaps wrapped with newlines from Node.js snapshots?
36+
if (!final.includes('\n') && fixedText.includes(makeEntry(`\n${final}\n`))) return
37+
}
38+
3439
return assert.fail(`Could not match "${key}" in snapshot. ${addFail}`)
3540
}
3641

tests/node/snapshot.test.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { describe, it } from 'node:test'
2+
import * as nodeTest from 'node:test'
3+
4+
const skip = !nodeTest.snapshot // Node.js 20.x and 22.12 (22.13 has it)
5+
const wrongSerialization = process.env.EXODUS_TEST_PLATFORM === 'xs' // https://github.com/Moddable-OpenSource/moddable/issues/1564
6+
7+
it('simple', { skip: skip || wrongSerialization }, (t) => {
8+
t.assert.snapshot(10)
9+
t.assert.snapshot(null)
10+
t.assert.snapshot()
11+
t.assert.snapshot([])
12+
t.assert.snapshot(/xx/)
13+
t.assert.snapshot(Infinity)
14+
t.assert.snapshot(false)
15+
t.assert.snapshot(true)
16+
t.assert.snapshot({})
17+
})
18+
19+
it('complex', { skip: skip || wrongSerialization }, (t) => {
20+
t.assert.snapshot([10])
21+
t.assert.snapshot([{ a: 10 }])
22+
t.assert.snapshot({ a: 10 })
23+
t.assert.snapshot({ a: 10, b: 20 })
24+
t.assert.snapshot(Buffer.from(''))
25+
})
26+
27+
// repeat test name
28+
it('simple', { skip: skip || wrongSerialization }, (t) => {
29+
t.assert.snapshot(/hello/)
30+
t.assert.snapshot(true)
31+
t.assert.snapshot(NaN)
32+
t.assert.snapshot({})
33+
t.assert.snapshot(42)
34+
t.assert.snapshot([])
35+
t.assert.snapshot(-Infinity)
36+
})
37+
38+
it('mixed', { skip: skip || wrongSerialization }, (t) => {
39+
t.assert.snapshot(true)
40+
t.assert.snapshot([1, 2, 3])
41+
t.assert.snapshot({ foo: 'bar' })
42+
t.assert.snapshot(43)
43+
t.assert.snapshot({})
44+
t.assert.snapshot([])
45+
})
46+
47+
// repeat test name
48+
it('mixed', { skip: skip || wrongSerialization }, (t) => {
49+
t.assert.snapshot([5, 4, 3])
50+
t.assert.snapshot([])
51+
t.assert.snapshot(false)
52+
t.assert.snapshot(41)
53+
t.assert.snapshot({ bar: 'buz' })
54+
t.assert.snapshot({})
55+
})
56+
57+
it('escape', { skip }, (t) => {
58+
t.assert.snapshot('\\')
59+
t.assert.snapshot('${')
60+
t.assert.snapshot('$$\\${')
61+
})
62+
63+
const TEST_ONE = { a: 20, d: Buffer.from('foo'), b: [1, 2, 'bar', 5], e: { foo: 'bar' } }
64+
// eslint-disable-next-line no-sparse-arrays
65+
const TEST_TWO = { ['__proto__']: [], b: [1, 2, , , 5], e: { foo: 'bar' }, f: -Infinity }
66+
const TEST_THREE = [new Error('?!'), new TypeError('bar'), new Uint16Array([4, 2, 65_123]), null]
67+
68+
// Test names repeat on a purpose!
69+
70+
it('test A', { skip }, (t) => {
71+
t.assert.snapshot(TEST_ONE)
72+
})
73+
74+
it('test B', { skip: skip || wrongSerialization }, (t) => {
75+
t.assert.snapshot(TEST_TWO)
76+
})
77+
78+
it('test B', { skip: skip || wrongSerialization }, (t) => {
79+
t.assert.snapshot(TEST_THREE)
80+
})
81+
82+
// Repeat name
83+
it('test B', { skip: skip || wrongSerialization }, (t) => {
84+
t.assert.snapshot({ x: 1337 })
85+
})
86+
87+
describe('nested test', { skip }, () => {
88+
it('test A', { skip: wrongSerialization }, (t) => {
89+
t.assert.snapshot(TEST_TWO)
90+
})
91+
92+
it('test A', { skip: wrongSerialization }, (t) => {
93+
t.assert.snapshot(TEST_THREE)
94+
})
95+
96+
it('nested test one', (t) => {
97+
t.assert.snapshot(TEST_ONE)
98+
})
99+
})
100+
101+
describe('weird names', { skip }, () => {
102+
const ascii = Array.from({ length: 128 })
103+
.fill()
104+
.map((a, i) => i)
105+
.slice(0x20)
106+
.map((i) => String.fromCodePoint(i))
107+
.join('')
108+
for (const key of [
109+
'\n',
110+
'{}',
111+
'$',
112+
'`',
113+
'>',
114+
'` ` `',
115+
'` `\n `',
116+
'\\',
117+
'\\\n`\n\\``',
118+
'${',
119+
ascii,
120+
]) {
121+
it(key, (t) => {
122+
t.assert.snapshot(key)
123+
})
124+
}
125+
126+
it('multi\nline', { skip }, (t) => {
127+
t.assert.snapshot(0)
128+
})
129+
130+
it('with `', { skip }, (t) => {
131+
t.assert.snapshot(42)
132+
})
133+
})
134+
135+
describe('', { skip }, () => {
136+
it('', (t) => {
137+
t.assert.snapshot('empty names test')
138+
t.assert.snapshot(t.fullName)
139+
t.assert.snapshot(t.name)
140+
})
141+
142+
it((t) => {
143+
t.assert.snapshot('no name test')
144+
t.assert.snapshot(t.fullName)
145+
t.assert.snapshot(t.name)
146+
})
147+
148+
// non-string names
149+
for (const name of [false, true, 0, 1, null, undefined, {}, ['arr'], 'str']) {
150+
it(name, (t) => {
151+
t.assert.snapshot(t.fullName)
152+
t.assert.snapshot(t.name)
153+
})
154+
}
155+
})

0 commit comments

Comments
 (0)