@exodus/test 1.0.0-rc.51 → 1.0.0-rc.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -76
- package/bin/index.js +9 -12
- package/package.json +6 -3
- package/src/engine.pure.cjs +22 -3
- package/src/engine.pure.snapshot.cjs +35 -0
- package/src/jest.config.js +2 -0
- package/src/jest.snapshot.js +5 -34
package/README.md
CHANGED
|
@@ -2,91 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
A runner for `node:test`, `jest`, and `tape` test suites on top of `node:test` (and any runtime)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Comes with typescript support, optional esm/cjs interop, and also loading babel transforms!
|
|
8
|
-
|
|
9
|
-
Use `--coverage` to generate coverage output
|
|
10
|
-
|
|
11
|
-
Default `NODE_ENV` value is "test", use `NODE_ENV=` to override (e.g. to empty)
|
|
12
|
-
|
|
13
|
-
## Why?
|
|
14
|
-
|
|
15
|
-
- Can run your tests on Node.js, Bun, Deno, JavaScriptCore and Hermes without extra churn
|
|
16
|
-
|
|
17
|
-
- Unlike `jest`, it is fast
|
|
18
|
-
|
|
19
|
-
- Unlike `node:test`, it is a drop-in replacement for `jest`
|
|
20
|
-
|
|
21
|
-
- With `expect`, support for snapshots, mocks and matchers
|
|
22
|
-
|
|
23
|
-
- `jest-when` and `jest-extended` are fully compatible and can just be used
|
|
24
|
-
|
|
25
|
-
- Snapshots are compatible with Jest and can just be used both ways
|
|
26
|
-
|
|
27
|
-
- Also compatible to `node:test`
|
|
28
|
-
|
|
29
|
-
- Unlike `bun:test`, it runs all test files in isolated contexts
|
|
30
|
-
|
|
31
|
-
Bun leaks globals / side effects between test files and has incompatible `test()` lifecycle / order
|
|
32
|
-
|
|
33
|
-
- Can use Jest config
|
|
34
|
-
|
|
35
|
-
- Native coverage support (enable via `--coverage`)
|
|
36
|
-
|
|
37
|
-
- Can record / replay `fetch` and `WebSocket` sessions. And run them on all runtimes (including Hermes)
|
|
38
|
-
|
|
39
|
-
- Automatic polyfills for JavaScriptCore / Hermes, including crypto
|
|
40
|
-
|
|
41
|
-
- Hanging tests error by default (unlike `jest`)
|
|
42
|
-
|
|
43
|
-
- Native ESM out of the box
|
|
5
|
+
## Features
|
|
44
6
|
|
|
7
|
+
- Native ESM, including in Jest tests
|
|
45
8
|
- Esbuild on the fly for babelified ESM interop (enable via `--esbuild`)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
9
|
+
- TypeScript support in both transform (through [tsx](https://tsx.is/), enable via `--esbuild`)
|
|
10
|
+
and typestrip (via `--typescript`) modes
|
|
11
|
+
- Runs on [node:test](https://nodejs.org/api/test.html), and (experimental) on bun, deno, d8, JSC and
|
|
12
|
+
[Hermes](https://hermesengine.dev)
|
|
13
|
+
- Testsuite-agnostic -- can run any file as long as it sets exit code based on test results
|
|
14
|
+
- Built-in [Jest](https://jestjs.io) compatibility (with `--jest`), including `jest.*` global
|
|
15
|
+
- Up to ~10x faster depending on the original setup
|
|
16
|
+
- Actual `expect` module, also `jest-extended` and `jest-when` just work on top
|
|
17
|
+
- Snapshots, including snapshot matchers
|
|
18
|
+
- Function and timer mocks
|
|
19
|
+
- [test.concurrent]()
|
|
20
|
+
- Module mocks (on top of Node.js runtime only), including for ESM modules
|
|
21
|
+
- Loads Jest configuration
|
|
22
|
+
- It works on Hermes too!
|
|
23
|
+
- Built-in network record/replay for offline tests, mocking `fetch` and `WebSocket` sessions
|
|
51
24
|
- `--drop-network` support for guaranteed offline testing
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
```js
|
|
80
|
-
import test from '@exodus/test/tape'
|
|
25
|
+
- Native code coverage via v8 (Node.js or [c8](https://github.com/bcoe/c8)), with istanbul reporters
|
|
26
|
+
- GitHub reporter (auto-enabled by default)
|
|
27
|
+
- JSDOM env support
|
|
28
|
+
- Hanging tests error by default (unlike `jest`)
|
|
29
|
+
- Babel support, picks up your Babel config (enable via `--babel`)
|
|
30
|
+
- Unlike `bun:test`, it runs test files in isolated contexts \
|
|
31
|
+
Bun leaks globals / side effects between test files ([ref](https://github.com/oven-sh/bun/issues/6024)),
|
|
32
|
+
and has incompatible `test()` lifecycle / order
|
|
33
|
+
- Also features a tape API for drop-in replacement
|
|
34
|
+
|
|
35
|
+
## Reporter samples
|
|
36
|
+
|
|
37
|
+
#### CLI (but uses colors when output supports them, e.g. in terminal):
|
|
38
|
+
|
|
39
|
+
```console
|
|
40
|
+
# tests/jest/expect.mock.test.js
|
|
41
|
+
✔ PASS drinkAll > drinks something lemon-flavoured (1.300417ms)
|
|
42
|
+
✔ PASS drinkAll > does not drink something octopus-flavoured (0.191791ms)
|
|
43
|
+
✔ PASS drinkAll (1.842959ms)
|
|
44
|
+
✔ PASS drinkEach > drinkEach drinks each drink (0.360625ms)
|
|
45
|
+
✔ PASS drinkEach (0.463416ms)
|
|
46
|
+
✔ PASS toHaveBeenCalledWith > registration applies correctly to orange La Croix (0.53325ms)
|
|
47
|
+
✔ PASS toHaveBeenCalledWith (0.564166ms)
|
|
48
|
+
✔ PASS toHaveBeenLastCalledWith > applying to all flavors does mango last (0.380375ms)
|
|
49
|
+
✔ PASS toHaveBeenLastCalledWith (0.473417ms)
|
|
50
|
+
# tests/jest/fn.invocationCallOrder.test.js
|
|
51
|
+
✔ PASS mock.invocationCallOrder (4.221042ms)
|
|
81
52
|
```
|
|
82
53
|
|
|
83
|
-
|
|
54
|
+
#### GitHub Actions collapses test results per-file, like this:
|
|
55
|
+
|
|
56
|
+
<details>
|
|
57
|
+
<summary>✅ <strong>tests/jest/lifecycle.test.js</strong></summary>
|
|
58
|
+
<pre>
|
|
59
|
+
✔ PASS A > B > C (3.26166ms)
|
|
60
|
+
✔ PASS A > B > D (1.699463ms)
|
|
61
|
+
✔ PASS A > B (6.72719ms)
|
|
62
|
+
✔ PASS A > E > F (1.117997ms)
|
|
63
|
+
✔ PASS A > E > G > H (1.330904ms)
|
|
64
|
+
✔ PASS A > E > G (1.94971ms)
|
|
65
|
+
✔ PASS A > E (3.821825ms)
|
|
66
|
+
✔ PASS A > I (0.533096ms)
|
|
67
|
+
✔ PASS A (13.887889ms)
|
|
68
|
+
✔ PASS J (0.373187ms)
|
|
69
|
+
✔ PASS K > L (0.659852ms)
|
|
70
|
+
✔ PASS K (1.143195ms)
|
|
71
|
+
</pre>
|
|
72
|
+
</details><details>
|
|
73
|
+
<summary>✅ <strong>tests/jest/timers.async.test.js</strong></summary>
|
|
74
|
+
<pre>
|
|
75
|
+
✔ PASS advanceTimersByTime() does not let microtasks to pass (5.326604ms)
|
|
76
|
+
✔ PASS advanceTimersByTime() does not let microtasks to pass even with await (1.336064ms)
|
|
77
|
+
✔ PASS advanceTimersByTimeAsync() lets microtasks to pass (6.99526ms)
|
|
78
|
+
✔ PASS advanceTimersByTimeAsync() lets microtasks to pass, chained (10.131664ms)
|
|
79
|
+
✔ PASS advanceTimersByTimeAsync() lets microtasks to pass, longer chained (8.635472ms)
|
|
80
|
+
✔ PASS advanceTimersByTimeAsync() lets microtasks to pass, async chain (56.937983ms)
|
|
81
|
+
</pre>
|
|
82
|
+
</details>
|
|
83
|
+
|
|
84
|
+
See live output in [CI](https://github.com/ExodusMovement/test/actions/workflows/checks.yaml)
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
## Library
|
|
86
87
|
|
|
87
88
|
### List of exports
|
|
88
89
|
|
|
89
|
-
- `@exodus/test/
|
|
90
|
+
- `@exodus/test/node` -- `node:test` API, working under non-Node.js platforms
|
|
91
|
+
|
|
92
|
+
- `@exodus/test/jest` -- `jest` implementation
|
|
90
93
|
|
|
91
94
|
- `@exodus/test/tape` -- `tape` mock (can also be helpful when moving from `tap`)
|
|
92
95
|
|
package/bin/index.js
CHANGED
|
@@ -16,9 +16,7 @@ import glob from 'fast-glob'
|
|
|
16
16
|
import { haveModuleMocks, haveSnapshots, haveForceExit } from '../src/version.js'
|
|
17
17
|
|
|
18
18
|
const bindir = dirname(fileURLToPath(import.meta.url))
|
|
19
|
-
|
|
20
|
-
const EXTS = `.?([cm])[jt]s?(x)` // we differ from jest, allowing [cm] before everything
|
|
21
|
-
const DEFAULT_PATTERNS = [`**/__tests__/**/*${EXTS}`, `**/?(*.)+(spec|test)${EXTS}`]
|
|
19
|
+
const DEFAULT_PATTERNS = [`**/?(*.)+(spec|test).?([cm])[jt]s?(x)`] // do not trust magic dirs by default
|
|
22
20
|
|
|
23
21
|
const bundleOptions = { pure: true, bundle: true, esbuild: true, ts: 'auto' }
|
|
24
22
|
const hermesAv = ['-Og', '-Xmicrotask-queue']
|
|
@@ -191,10 +189,18 @@ const { options, patterns } = parseOptions()
|
|
|
191
189
|
const warnHuman = isTTY && !isCI ? (...args) => console.warn(...args) : () => {}
|
|
192
190
|
if (isCI) process.env.FORCE_COLOR = '1' // should support colored output even though not a TTY, overridable with --no-color
|
|
193
191
|
|
|
192
|
+
const setEnv = (name, value) => {
|
|
193
|
+
const env = process.env[name]
|
|
194
|
+
if (env && env !== value) throw new Error(`env conflict: ${name}="${env}", effective: "${value}"`)
|
|
195
|
+
process.env[name] = value
|
|
196
|
+
}
|
|
197
|
+
|
|
194
198
|
const engineOptions = ENGINES.get(options.engine)
|
|
195
199
|
assert(engineOptions, `Unknown engine: ${options.engine}`)
|
|
196
200
|
Object.assign(options, engineOptions)
|
|
197
201
|
options.platform = options.binary // binary can be overriden by c8
|
|
202
|
+
setEnv('EXODUS_TEST_ENGINE', options.engine) // e.g. 'hermes:bundle', 'node:bundle', 'node:test', 'node:pure'
|
|
203
|
+
setEnv('EXODUS_TEST_PLATFORM', options.binary) // e.g. 'hermes', 'node'
|
|
198
204
|
|
|
199
205
|
const require = createRequire(import.meta.url)
|
|
200
206
|
const resolveRequire = (query) => require.resolve(query)
|
|
@@ -406,15 +412,6 @@ if (!options.bundle) {
|
|
|
406
412
|
}
|
|
407
413
|
|
|
408
414
|
if (!Object.hasOwn(process.env, 'NODE_ENV')) process.env.NODE_ENV = 'test'
|
|
409
|
-
|
|
410
|
-
const setEnv = (name, value) => {
|
|
411
|
-
const env = process.env[name]
|
|
412
|
-
if (env && env !== value) throw new Error(`env conflict: ${name}="${env}", effective: "${value}"`)
|
|
413
|
-
process.env[name] = value
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
setEnv('EXODUS_TEST_PLATFORM', options.binary) // e.g. 'hermes', 'node'
|
|
417
|
-
setEnv('EXODUS_TEST_ENGINE', options.engine) // e.g. 'hermes:bundle', 'node:bundle', 'node:test', 'node:pure'
|
|
418
415
|
setEnv('EXODUS_TEST_ONLY', options.only ? '1' : '')
|
|
419
416
|
|
|
420
417
|
const c8 = resolveRequire('c8/bin/c8.js')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/test",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.52",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"src/engine.js",
|
|
65
65
|
"src/engine.node.cjs",
|
|
66
66
|
"src/engine.pure.cjs",
|
|
67
|
+
"src/engine.pure.snapshot.cjs",
|
|
67
68
|
"src/engine.select.cjs",
|
|
68
69
|
"src/jest.js",
|
|
69
70
|
"src/jest.config.js",
|
|
@@ -87,8 +88,9 @@
|
|
|
87
88
|
"test:typescript": "./bin/index.js --jest --typescript tests/typescript.test.ts",
|
|
88
89
|
"test:jest": "./bin/index.js --jest --esbuild",
|
|
89
90
|
"test:tape": "./bin/index.js --esbuild 'tests/tape/tests/*.js' tests/tape.test.js",
|
|
90
|
-
"test:pure": "EXODUS_TEST_ENGINE=node:pure npm run test",
|
|
91
|
-
"test:bun
|
|
91
|
+
"test:pure": "EXODUS_TEST_ENGINE=node:pure EXODUS_TEST_IGNORE='tests/jest-extended/**' npm run test",
|
|
92
|
+
"test:bun": "EXODUS_TEST_ENGINE=bun:pure EXODUS_TEST_IGNORE='tests/jest-extended/**' npm run test",
|
|
93
|
+
"test:hermes": "EXODUS_TEST_ENGINE=hermes:bundle EXODUS_TEST_IGNORE='tests/{{jest-extended,inband}/**,jest-when/when.test.*,jest/jest.{mock,resetModules}.*}' npm run test",
|
|
92
94
|
"test:fetch": "./bin/index.js --jest --drop-network --engine node:pure tests/fetch.test.js tests/websocket.test.js",
|
|
93
95
|
"coverage": "./bin/index.js --jest --esbuild --coverage",
|
|
94
96
|
"lint": "prettier --list-different . && eslint .",
|
|
@@ -137,6 +139,7 @@
|
|
|
137
139
|
"@types/jest-when": "^3.5.2",
|
|
138
140
|
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
|
139
141
|
"eslint": "^8.44.0",
|
|
142
|
+
"hermes-engine-cli": "^0.12.0",
|
|
140
143
|
"jest": "^29.7.0",
|
|
141
144
|
"jest-matcher-utils": "^29.7.0",
|
|
142
145
|
"jest-serializer-ansi-escapes": "^3.0.0",
|
package/src/engine.pure.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const assert = require('node:assert/strict')
|
|
2
2
|
const assertLoose = require('node:assert')
|
|
3
|
+
const { matchSnapshot } = require('./engine.pure.snapshot.cjs')
|
|
3
4
|
|
|
4
5
|
const { setTimeout, setInterval, setImmediate, Date } = globalThis
|
|
5
6
|
const { clearTimeout, clearInterval, clearImmediate } = globalThis
|
|
@@ -28,17 +29,21 @@ class Context {
|
|
|
28
29
|
children = []
|
|
29
30
|
assert = { ...assertLoose, snapshot: undefined }
|
|
30
31
|
hooks = { __proto__: null, before: [], after: [], beforeEach: [], afterEach: [] }
|
|
32
|
+
#fullName
|
|
31
33
|
|
|
32
34
|
constructor(parent, name, options = {}) {
|
|
33
35
|
Object.assign(this, { root: parent?.root, parent, name, options })
|
|
34
|
-
this
|
|
35
|
-
if (this
|
|
36
|
+
this.#fullName = parent && parent !== parent.root ? `${parent.fullName} > ${name}` : name
|
|
37
|
+
if (this.#fullName === name) this.#fullName = this.#fullName.replace(INBAND_PREFIX_REGEX, '')
|
|
36
38
|
if (this.root) {
|
|
37
39
|
this.parent.children.push(this)
|
|
38
40
|
} else {
|
|
39
41
|
assert(this.name === '<root>' && !this.parent)
|
|
40
42
|
this.root = this
|
|
41
43
|
}
|
|
44
|
+
|
|
45
|
+
this.assert.snapshot = (obj) =>
|
|
46
|
+
matchSnapshot(readSnapshot, assert, this.fullName, serializeSnapshot(obj))
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
get onlySomewhere() {
|
|
@@ -48,6 +53,10 @@ class Context {
|
|
|
48
53
|
get only() {
|
|
49
54
|
return (this.options.only && !this.children.some((x) => x.onlySomewhere)) || this.parent?.only
|
|
50
55
|
}
|
|
56
|
+
|
|
57
|
+
get fullName() {
|
|
58
|
+
return this.#fullName
|
|
59
|
+
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
function enterContext(name, options) {
|
|
@@ -466,7 +475,17 @@ if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
|
466
475
|
|
|
467
476
|
// eslint-disable-next-line no-undef
|
|
468
477
|
let snapshotResolver = (dir, name) => [dir, `${name}.snapshot`] // default per Node.js docs
|
|
469
|
-
|
|
478
|
+
let snapshotSerializers = [(obj) => JSON.stringify(obj, null, 2)]
|
|
479
|
+
const serializeSnapshot = (obj) => {
|
|
480
|
+
let val = obj
|
|
481
|
+
for (const fn of snapshotSerializers) val = fn(val)
|
|
482
|
+
return val
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const setSnapshotSerializers = ([...arr]) => {
|
|
486
|
+
snapshotSerializers = arr
|
|
487
|
+
}
|
|
488
|
+
|
|
470
489
|
const setSnapshotResolver = (fn) => {
|
|
471
490
|
snapshotResolver = fn
|
|
472
491
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const nameCounts = new Map()
|
|
2
|
+
let snapshotText
|
|
3
|
+
|
|
4
|
+
const escapeSnapshot = (str) => str.replaceAll(/([\\`])/gu, '\\$1')
|
|
5
|
+
|
|
6
|
+
function matchSnapshot(readSnapshot, assert, name, serialized) {
|
|
7
|
+
// We don't have native snapshots, polyfill reading
|
|
8
|
+
if (snapshotText !== null) {
|
|
9
|
+
try {
|
|
10
|
+
const snapshotRaw = readSnapshot()
|
|
11
|
+
snapshotText = snapshotRaw ? `\n${snapshotRaw}\n` : null // we'll search wrapped in \n
|
|
12
|
+
} catch {
|
|
13
|
+
snapshotText = null
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const addFail = `Adding new snapshots requires Node.js >=22.3.0`
|
|
18
|
+
|
|
19
|
+
// We don't support polyfilled snapshot generation here, only parsing
|
|
20
|
+
// Also be careful with assertion plan counters
|
|
21
|
+
if (!snapshotText) assert.fail(`Could not find snapshot file. ${addFail}`)
|
|
22
|
+
|
|
23
|
+
const count = (nameCounts.get(name) || 0) + 1
|
|
24
|
+
nameCounts.set(name, count)
|
|
25
|
+
const escaped = escapeSnapshot(serialized)
|
|
26
|
+
const key = `${name} ${count}`
|
|
27
|
+
const makeEntry = (x) => `\nexports[\`${escapeSnapshot(key)}\`] = \`${x}\`;\n`
|
|
28
|
+
const final = escaped.includes('\n') ? `\n${escaped}\n` : escaped
|
|
29
|
+
if (snapshotText.includes(makeEntry(final))) return
|
|
30
|
+
// Perhaps wrapped with newlines from Node.js snapshots?
|
|
31
|
+
if (!final.includes('\n') && snapshotText.includes(makeEntry(`\n${final}\n`))) return
|
|
32
|
+
return assert.fail(`Could not match "${key}" in snapshot. ${addFail}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { escapeSnapshot, matchSnapshot }
|
package/src/jest.config.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import { specialEnvironments } from './jest.environment.js'
|
|
4
4
|
|
|
5
|
+
const EXTS = `.?([cm])[jt]s?(x)` // we differ from jest, allowing [cm] before everything
|
|
5
6
|
const normalizeJestConfig = (config) => ({
|
|
7
|
+
testMatch: [`**/__tests__/**/*${EXTS}`, `**/?(*.)+(spec|test)${EXTS}`],
|
|
6
8
|
testEnvironment: 'node',
|
|
7
9
|
testTimeout: 5000,
|
|
8
10
|
testPathIgnorePatterns: [],
|
package/src/jest.snapshot.js
CHANGED
|
@@ -11,6 +11,7 @@ import { format, plugins as builtinPlugins } from 'pretty-format'
|
|
|
11
11
|
import { jestConfig } from './jest.config.js'
|
|
12
12
|
import { getTestNamePath } from './dark.cjs'
|
|
13
13
|
import { haveSnapshotsReportUnescaped } from './version.js'
|
|
14
|
+
import { matchSnapshot, escapeSnapshot } from './engine.pure.snapshot.cjs'
|
|
14
15
|
|
|
15
16
|
const { snapshotFormat, snapshotSerializers } = jestConfig()
|
|
16
17
|
const plugins = Object.values(builtinPlugins)
|
|
@@ -19,10 +20,6 @@ const serialize = (val) => format(val, { ...snapshotFormat, plugins }).replaceAl
|
|
|
19
20
|
let serializersAreSetup = false
|
|
20
21
|
let snapshotsAreJest = false
|
|
21
22
|
|
|
22
|
-
// For manually loading the snapshot
|
|
23
|
-
const nameCounts = new Map()
|
|
24
|
-
let snapshotText
|
|
25
|
-
|
|
26
23
|
function maybeSetupSerializers() {
|
|
27
24
|
if (serializersAreSetup) return
|
|
28
25
|
// empty require and serializers should not let this fail, non-empty serializers and empty require should
|
|
@@ -48,6 +45,7 @@ const wrap = (check) => {
|
|
|
48
45
|
}
|
|
49
46
|
}
|
|
50
47
|
|
|
48
|
+
// Older Node.js versions do not have context.assert, which we want to prefer for counting
|
|
51
49
|
let context
|
|
52
50
|
beforeEach((t) => (context = t))
|
|
53
51
|
const getAssert = () => context?.assert ?? assert // do not use non-strict comparisons on this!
|
|
@@ -123,47 +121,20 @@ const snapOnDisk = (orig, matcher) => {
|
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
const obj = matcher ? deepMerge(orig, matcher) : orig
|
|
126
|
-
const escape = (str) => str.replaceAll(/([\\`])/gu, '\\$1')
|
|
127
124
|
|
|
128
125
|
maybeSetupJestSnapshots()
|
|
129
126
|
|
|
130
127
|
if (!context?.assert?.snapshot) {
|
|
131
|
-
// We don't have native snapshots, polyfill reading
|
|
132
|
-
if (snapshotText !== null) {
|
|
133
|
-
try {
|
|
134
|
-
const snapshotRaw = readSnapshot()
|
|
135
|
-
snapshotText = snapshotRaw ? `\n${snapshotRaw}\n` : null // we'll search wrapped in \n
|
|
136
|
-
} catch {
|
|
137
|
-
snapshotText = null
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const addFail = `Adding new snapshots requires Node.js >=22.3.0`
|
|
142
|
-
|
|
143
|
-
// We don't support polyfilled snapshot generation here, only parsing
|
|
144
|
-
// Also be careful with assertion plan counters
|
|
145
|
-
if (!snapshotText) getAssert().fail(`Could not find snapshot file. ${addFail}`)
|
|
146
|
-
|
|
147
128
|
const namePath = getTestNamePath(context).map((x) => (x === '<anonymous>' ? '' : x))
|
|
148
|
-
|
|
149
|
-
const count = (nameCounts.get(name) || 0) + 1
|
|
150
|
-
nameCounts.set(name, count)
|
|
151
|
-
const escaped = escape(serialize(obj))
|
|
152
|
-
const key = `${name} ${count}`
|
|
153
|
-
const makeEntry = (x) => `\nexports[\`${escape(key)}\`] = \`${x}\`;\n`
|
|
154
|
-
const final = escaped.includes('\n') ? `\n${escaped}\n` : escaped
|
|
155
|
-
if (snapshotText.includes(makeEntry(final))) return
|
|
156
|
-
// Perhaps wrapped with newlines from Node.js snapshots?
|
|
157
|
-
if (!final.includes('\n') && snapshotText.includes(makeEntry(`\n${final}\n`))) return
|
|
158
|
-
return getAssert().fail(`Could not match "${key}" in snapshot. ${addFail}`)
|
|
129
|
+
return matchSnapshot(readSnapshot, getAssert(), namePath.join(' '), serialize(obj))
|
|
159
130
|
}
|
|
160
131
|
|
|
161
132
|
// Node.js always wraps with newlines, while jest wraps only those that are already multiline
|
|
162
133
|
try {
|
|
163
|
-
wrapContextName(() =>
|
|
134
|
+
wrapContextName(() => context.assert.snapshot(obj))
|
|
164
135
|
} catch (e) {
|
|
165
136
|
if (typeof e.expected === 'string') {
|
|
166
|
-
const escaped = haveSnapshotsReportUnescaped ? e.expected :
|
|
137
|
+
const escaped = haveSnapshotsReportUnescaped ? e.expected : escapeSnapshot(e.expected)
|
|
167
138
|
const final = escaped.includes('\n') ? escaped : `\n${escaped}\n`
|
|
168
139
|
if (final === e.actual) return
|
|
169
140
|
}
|