@exodus/test 1.0.0-rc.104 → 1.0.0-rc.106

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 CHANGED
@@ -168,11 +168,17 @@ Just use `"test": "exodus-test"`
168
168
 
169
169
  - `--test-force-exit` — force exit after tests are done
170
170
 
171
- ## Jest compatibility
171
+ ## Module mocking in ESM
172
172
 
173
- The `--jest` mode is mostly compatible with Jest. There are some noteworthy differences though.
174
- This tool does not hoist mocks, so it is important that a mock is defined before the module that uses it is imported.
175
- In ESM, this can be achieved with dynamic imports:
173
+ Module mocks in ESM is a common source of confusion, as Jest in most old setups does not run real ESM,
174
+ and instead uses Babel to transform ESM into CJS, and then hoists mocks on top of `require()` calls.
175
+
176
+ That hoisting is not possible in ESM world, as static import statements are always resolved before
177
+ any other code.
178
+
179
+ Also see [Jest documentation](https://jestjs.io/docs/ecmascript-modules#module-mocking-in-esm) on that.
180
+
181
+ To port code from CJS or Babel, e.g. the following approach with dynamic imports can be used:
176
182
 
177
183
  ```js
178
184
  jest.doMock('./hogwarts.js', () => ({
package/benchmark.js ADDED
@@ -0,0 +1 @@
1
+ export * from './src/benchmark.js'
@@ -4,7 +4,7 @@ import { join } from 'node:path'
4
4
  import { createRequire } from 'node:module'
5
5
 
6
6
  const require = createRequire(import.meta.url)
7
- const nvm = process.env.NVM_BIN ? (x) => join(process.env.NVM_BIN, '../lib/node_modules', x) : null
7
+ // const nvm = process.env.NVM_BIN ? (x) => join(process.env.NVM_BIN, '../lib/node_modules', x) : null
8
8
  const jsvu = (x) => join(homedir(), '.jsvu/bin', x)
9
9
  const esvu = (x) => join(homedir(), '.esvu/bin', x)
10
10
 
@@ -38,10 +38,9 @@ function findBinaryOnce(name) {
38
38
  const flavor = Object.hasOwn(flavors, process.platform) ? flavors[process.platform] : null
39
39
  return findFile([
40
40
  (bin) => flavor && require.resolve(`react-native/sdks/hermesc/${flavor}/${bin}`), // 1. Locally installed react-native dep (works only for osx)
41
- (bin) => flavor && require.resolve(`hermes-engine-cli/${flavor}/${bin}`), // 2. Locally installed hermes-engine-cli
42
- (bin) => jsvu(bin), // 3. jsvu
43
- (bin) => nvm(`hermes-engine-cli/${flavor}/${bin}`), // 4. hermes-engine-cli installed in .nvm dir with npm i -g
44
- ]) // 5. hermes installed in the system
41
+ (bin) => jsvu(bin), // 2. jsvu
42
+ (bin) => esvu(bin), // 3. esvu
43
+ ]) // 4. hermes installed in the system
45
44
  }
46
45
 
47
46
  case 'jsc':
@@ -67,6 +66,9 @@ function findBinaryOnce(name) {
67
66
  return require('electron')
68
67
  case 'workerd':
69
68
  return require.resolve('workerd/bin/workerd')
69
+ case 'jerryscript':
70
+ name = 'jerry' // look under this name, including in global
71
+ return findFile([jsvu, esvu])
70
72
  case 'c8':
71
73
  return require.resolve('c8/bin/c8.js')
72
74
  case 'chrome':
package/bin/index.js CHANGED
@@ -19,7 +19,7 @@ import { glob as globImplementation } from '../src/glob.cjs'
19
19
  const DEFAULT_PATTERNS = [`**/?(*.)+(spec|test).?([cm])[jt]s?(x)`] // do not trust magic dirs by default
20
20
  const bundleOpts = { pure: true, bundle: true, esbuild: true, ts: 'auto' }
21
21
  const bareboneOpts = { ...bundleOpts, barebone: true }
22
- const hermesA = ['-Og', '-Xmicrotask-queue']
22
+ const hermesA = ['-w', '-Xmicrotask-queue'] // -Xes6-class fails with -O0 / -Og, --block-scoping fails in default, any of that is bad
23
23
  const denoA = ['run', '--allow-all'] // also will set DENO_COMPAT=1 env flag below
24
24
  const denoT = ['test', '--allow-all']
25
25
  const nodeTS = process.features.typescript ? 'auto' : 'flag'
@@ -49,6 +49,7 @@ const ENGINES = new Map(
49
49
  'graaljs:bundle': { binary: 'graaljs', ...bareboneOpts },
50
50
  'escargot:bundle': { binary: 'escargot', ...bareboneOpts },
51
51
  'boa:bundle': { binary: 'boa', binaryArgs: ['-m'], ...bareboneOpts },
52
+ 'jerryscript:bundle': { binary: 'jerryscript', ...bareboneOpts },
52
53
  // Browser engines
53
54
  'chrome:puppeteer': { binary: 'chrome', browsers: 'puppeteer', ...bundleOpts },
54
55
  'firefox:puppeteer': { binary: 'firefox', browsers: 'puppeteer', ...bundleOpts },
@@ -62,7 +63,7 @@ const ENGINES = new Map(
62
63
  })
63
64
  )
64
65
  const barebonesOk = ['v8', 'd8', 'spidermonkey', 'quickjs', 'xs', 'hermes']
65
- const barebonesUnhandled = ['jsc', 'escargot', 'boa', 'graaljs', 'engine262']
66
+ const barebonesUnhandled = ['jsc', 'escargot', 'boa', 'graaljs', 'jerry', 'engine262']
66
67
 
67
68
  const getEnvFlag = (name) => {
68
69
  if (!Object.hasOwn(process.env, name)) return
@@ -294,9 +295,7 @@ assert(!options.throttle || options.browsers, engineFlagError('throttle-cpu'))
294
295
 
295
296
  const args = []
296
297
 
297
- if (have.haveModuleMocks && engineOptions.haveIsOk) {
298
- args.push('--experimental-test-module-mocks')
299
- }
298
+ if (engineOptions.haveIsOk) args.push('--experimental-test-module-mocks')
300
299
 
301
300
  if (options.pure) {
302
301
  if (options.bundle) {
@@ -322,11 +321,7 @@ if (options.pure) {
322
321
  args.push('--test-update-snapshots')
323
322
  }
324
323
 
325
- if (options.forceExit) {
326
- assert(have.haveForceExit && engineOptions.haveIsOk, 'For forceExit, use Node.js >= 20.14.0')
327
- args.push('--test-force-exit')
328
- }
329
-
324
+ if (options.forceExit) args.push('--test-force-exit')
330
325
  if (options.watch) args.push('--watch')
331
326
  if (options.only) args.push('--test-only')
332
327
 
@@ -766,8 +761,7 @@ if (options.pure) {
766
761
  assert(files.length > 0) // otherwise we can run recursively
767
762
  if (options.concurrency) args.push('--test-concurrency', options.concurrency)
768
763
  if (['--inspect', '--inspect-brk', '--inspect-wait'].includes(options.devtools)) {
769
- args.push(options.devtools)
770
- if (have.haveNetworkInspection) args.push('--experimental-network-inspection')
764
+ args.push(options.devtools, '--experimental-network-inspection')
771
765
  console.warn(
772
766
  ['--inspect-brk', '--inspect-wait'].includes(options.devtools)
773
767
  ? 'Open chrome://inspect/ to connect devtools, waiting'
package/mock.js ADDED
@@ -0,0 +1 @@
1
+ export * from './src/mock.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/test",
3
- "version": "1.0.0-rc.104",
3
+ "version": "1.0.0-rc.106",
4
4
  "author": "Exodus Movement, Inc.",
5
5
  "description": "A test suite runner",
6
6
  "homepage": "https://github.com/ExodusMovement/test",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "license": "MIT",
21
21
  "engines": {
22
- "node": "^18.19.0 || ^20.8.0 || >=22.0.0"
22
+ "node": "^20.18.0 || >=22.6.0"
23
23
  },
24
24
  "type": "module",
25
25
  "bin": {
@@ -31,6 +31,7 @@
31
31
  "./benchmark": "./src/benchmark.js",
32
32
  "./expect": "./src/expect.cjs",
33
33
  "./jest": "./src/jest.js",
34
+ "./mock": "./src/mock.js",
34
35
  "./node": "./src/node.js",
35
36
  "./tape": {
36
37
  "import": "./src/tape.js",
@@ -74,6 +75,7 @@
74
75
  "src/jest.setup.js",
75
76
  "src/jest.snapshot.js",
76
77
  "src/jest.timers.js",
78
+ "src/mock.js",
77
79
  "src/node.js",
78
80
  "src/pretty-format.cjs",
79
81
  "src/replay.js",
@@ -81,8 +83,10 @@
81
83
  "src/tape.cjs",
82
84
  "src/timers-track.js",
83
85
  "src/version.js",
86
+ "benchmark.js",
84
87
  "expect.cjs",
85
88
  "jest.js",
89
+ "mock.js",
86
90
  "node.js",
87
91
  "tape.js",
88
92
  "CHANGELOG.md"
@@ -125,6 +129,7 @@
125
129
  "test:graaljs": "EXODUS_TEST_ENGINE=graaljs:bundle npm run test:_bundle --",
126
130
  "test:escargot": "EXODUS_TEST_ENGINE=escargot:bundle npm run test:_bundle --",
127
131
  "test:boa": "EXODUS_TEST_ENGINE=boa:bundle npm run test:_bundle --",
132
+ "test:jerryscript": "EXODUS_TEST_ENGINE=jerryscript:bundle npm run test:_bundle -- --bundle-entropy-size 128",
128
133
  "test:fetch": "node ./bin/index.js --jest --drop-network --engine node:pure 'tests/replay/*.test.js'",
129
134
  "test:jsdom": "EXODUS_TEST_JEST_CONFIG='{\"testMatch\":[\"**/*.jsdom-test.js\"],\"testEnvironment\":\"jsdom\", \"rootDir\": \".\"}' ./bin/index.js --jest",
130
135
  "coverage": "node ./bin/index.js --jest --esbuild --coverage",
@@ -138,15 +143,15 @@
138
143
  "optionalDependencies": {
139
144
  "@chalker/queue": "^1.0.1",
140
145
  "@exodus/replay": "^1.0.0-rc.9",
141
- "@exodus/test-bundler": "1.0.0-rc.6",
146
+ "@exodus/test-bundler": "1.0.0-rc.7",
142
147
  "c8": "^9.1.0",
143
- "expect": "^29.7.0",
148
+ "expect": "^30.2.0",
144
149
  "fast-glob": "^3.2.11",
145
150
  "jest-extended": "^4.0.2",
146
151
  "playwright-core": "^1.52.0",
147
- "pretty-format": "^29.7.0",
148
- "puppeteer-core": "^24.6.0",
149
- "tsx": "^4.20.3"
152
+ "pretty-format": "^30.2.0",
153
+ "puppeteer-core": "^24.14.0",
154
+ "tsx": "^4.20.6"
150
155
  },
151
156
  "devDependencies": {
152
157
  "@exodus/eslint-config": "^5.24.0",
package/src/benchmark.js CHANGED
@@ -10,9 +10,10 @@ const fTime = (ns) => {
10
10
  return min < 5n ? `${s}s` : `${min}min`
11
11
  }
12
12
 
13
+ const { performance, scheduler, process, requestAnimationFrame, gc } = globalThis
13
14
  const getTime = (() => {
14
- if (globalThis.process) return () => process.hrtime.bigint()
15
- if (globalThis.performance) return () => BigInt(Math.round(performance.now() * 1e6))
15
+ if (process) return () => process.hrtime.bigint()
16
+ if (performance) return () => BigInt(Math.round(performance.now() * 1e6))
16
17
  return () => BigInt(Math.round(Date.now() * 1e6))
17
18
  })()
18
19
 
@@ -22,9 +23,13 @@ export async function benchmark(name, options, fn) {
22
23
  if (options?.skip) return
23
24
  const { args, timeout = 1000 } = options ?? {}
24
25
 
25
- if (globalThis.gc) {
26
- for (let i = 0; i < 4; i++) globalThis.gc()
27
- } else if (!gcWarned) {
26
+ // This will pause us for a bit, but we don't care - having a non-busy process is more important
27
+ await new Promise((resolve) => setTimeout(resolve, 0))
28
+ if (requestAnimationFrame) await new Promise((resolve) => requestAnimationFrame(resolve))
29
+ if (scheduler?.yield) await scheduler.yield()
30
+
31
+ if (gc) for (let i = 0; i < 4; i++) gc()
32
+ if (!gc && !gcWarned) {
28
33
  gcWarned = true
29
34
  console.log('Warning: no gc() available\n')
30
35
  }
@@ -43,12 +48,14 @@ export async function benchmark(name, options, fn) {
43
48
  total += diff
44
49
  if (min === undefined || min > diff) min = diff
45
50
  if (max === undefined || max < diff) max = diff
46
- if (total >= BigInt(timeout)) break
51
+ if (total >= BigInt(timeout) * 10n ** 6n) break
47
52
  }
48
53
 
49
54
  const mean = total / BigInt(count)
50
- const rps = 1e9 / Number(mean)
51
- console.log(`${name} x ${fRps(rps)} ops/sec @ ${fTime(mean)}/op (${fTime(min)}..${fTime(max)})`)
55
+ let res = `${name} x ${fRps(1e9 / Number(mean))} ops/sec @ ${fTime(mean)}/op`
56
+ if (fTime(min) !== fTime(max)) res += ` (${fTime(min)}..${fTime(max)})`
57
+ console.log(res)
52
58
 
53
- if (globalThis.gc) for (let i = 0; i < 4; i++) globalThis.gc()
59
+ if (gc) for (let i = 0; i < 4; i++) gc()
60
+ return { total, count, mean, min, max }
54
61
  }
package/src/dark.cjs CHANGED
@@ -52,57 +52,63 @@ function createCallerLocationHook() {
52
52
  return { installLocationInNextTest, getCallerLocation }
53
53
  }
54
54
 
55
- // Easy on Node.js >= 22.3.0 || ^20.16.0, but we polyfill for the rest
56
- function getTestNamePath(t) {
57
- // No implementation in Node.js yet, will have to PR
58
- if (t.fullName) return t.fullName.split(' > ')
59
-
60
- if (process.env.EXODUS_TEST_ENGINE === 'node:test') {
61
- // We are on Node.js < 22.3.0 where even t.fullName doesn't exist yet, polyfill
62
- const namePath = Symbol('namePath')
63
- const getNamePath = Symbol('getNamePath')
64
- try {
65
- if (t[namePath]) return t[namePath]
66
-
67
- // Sigh, ok, whatever
68
- const { Test } = require('node:internal/test_runner/test')
55
+ // Optimized out in 'bundle' env
56
+ function getTestNamePathFromNode(t) {
57
+ // We are on Node.js < 22.3.0 where even t.fullName doesn't exist yet, polyfill
58
+ const namePath = Symbol('namePath')
59
+ const getNamePath = Symbol('getNamePath')
60
+ try {
61
+ if (t[namePath]) return t[namePath]
62
+
63
+ // Sigh, ok, whatever
64
+ const { Test } = require('node:internal/test_runner/test')
65
+
66
+ const usePathName = Symbol('usePathName')
67
+ const restoreName = Symbol('restoreName')
68
+ Test.prototype[getNamePath] = function () {
69
+ if (this === this.root) return []
70
+ return [...(this.parent?.[getNamePath]() || []), this.name]
71
+ }
69
72
 
70
- const usePathName = Symbol('usePathName')
71
- const restoreName = Symbol('restoreName')
72
- Test.prototype[getNamePath] = function () {
73
- if (this === this.root) return []
74
- return [...(this.parent?.[getNamePath]() || []), this.name]
73
+ const diagnostic = Test.prototype.diagnostic
74
+ Test.prototype.diagnostic = function (...args) {
75
+ if (args[0] === usePathName) {
76
+ this[restoreName] = this.name
77
+ this.name = this[getNamePath]()
78
+ return
75
79
  }
76
80
 
77
- const diagnostic = Test.prototype.diagnostic
78
- Test.prototype.diagnostic = function (...args) {
79
- if (args[0] === usePathName) {
80
- this[restoreName] = this.name
81
- this.name = this[getNamePath]()
82
- return
83
- }
81
+ if (args[0] === restoreName) {
82
+ this.name = this[restoreName]
83
+ delete this[restoreName]
84
+ return
85
+ }
84
86
 
85
- if (args[0] === restoreName) {
86
- this.name = this[restoreName]
87
- delete this[restoreName]
88
- return
89
- }
87
+ return diagnostic.apply(this, args)
88
+ }
90
89
 
91
- return diagnostic.apply(this, args)
92
- }
90
+ const TestContextProto = Object.getPrototypeOf(t)
91
+ Object.defineProperty(TestContextProto, namePath, {
92
+ get() {
93
+ this.diagnostic(usePathName)
94
+ const result = this.name
95
+ this.diagnostic(restoreName)
96
+ return result
97
+ },
98
+ })
99
+
100
+ return t[namePath]
101
+ } catch {}
102
+ }
93
103
 
94
- const TestContextProto = Object.getPrototypeOf(t)
95
- Object.defineProperty(TestContextProto, namePath, {
96
- get() {
97
- this.diagnostic(usePathName)
98
- const result = this.name
99
- this.diagnostic(restoreName)
100
- return result
101
- },
102
- })
104
+ // Easy on Node.js >= 22.3.0 || ^20.16.0, but we polyfill for the rest
105
+ function getTestNamePath(t) {
106
+ // No implementation in Node.js yet, will have to PR
107
+ if (t.fullName) return t.fullName.split(' > ')
103
108
 
104
- return t[namePath]
105
- } catch {}
109
+ if (process.env.EXODUS_TEST_ENGINE === 'node:test') {
110
+ const names = getTestNamePathFromNode(t)
111
+ if (names) return names
106
112
  }
107
113
 
108
114
  return [t.name] // last resort
@@ -30,7 +30,14 @@ const setSnapshotResolver = (fn) => {
30
30
  }
31
31
 
32
32
  const mockModule = mock?.module
33
- ? (t, o) => mock.module(t.includes('\\') ? pathToFileURL(t) : t, o) // resolve windows-looking paths
33
+ ? (t, o) => {
34
+ try {
35
+ // resolve windows-looking paths, fails on old 20.x/22.x, but non-url works there, hence in try-catch
36
+ if (t.includes('\\')) return mock.module(pathToFileURL(t), o)
37
+ } catch {}
38
+
39
+ return mock.module(t, o)
40
+ }
34
41
  : undefined
35
42
 
36
43
  /* eslint-disable unicorn/no-useless-spread */
@@ -539,7 +539,7 @@ if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
539
539
  // eslint-disable-next-line no-undef
540
540
  const bundleSnaps = typeof EXODUS_TEST_SNAPSHOTS !== 'undefined' && new Map(EXODUS_TEST_SNAPSHOTS)
541
541
  const resolveSnapshot = (f) => snapshotResolver(f[0], f[1]).join('/')
542
- readSnapshot = (f = baseFile) => (f ? bundleSnaps.get(resolveSnapshot(f)) : null)
542
+ readSnapshot = (f = baseFile) => (f && bundleSnaps?.get(resolveSnapshot(f))) || null
543
543
  utilFormat = require('exodus-test:util-format')
544
544
  } else {
545
545
  const { existsSync, readFileSync } = require('node:fs')
@@ -34,4 +34,4 @@ function matchSnapshot(readSnapshot, assert, name, serialized) {
34
34
  return assert.fail(`Could not match "${key}" in snapshot. ${addFail}`)
35
35
  }
36
36
 
37
- module.exports = { escapeSnapshot, matchSnapshot }
37
+ module.exports = { matchSnapshot }
package/src/exodus.js CHANGED
@@ -1,51 +1,24 @@
1
1
  import { mock } from './engine.js'
2
2
  import * as node from './engine.js'
3
- import { fetchReplay, fetchRecord, websocketRecord, websocketReplay } from './replay.js'
4
- import { timersTrack, timersList, timersDebug, timersAssert } from './timers-track.js'
5
- import { insideEsbuild } from './dark.cjs'
6
- import { haveValidTimers } from './version.js'
7
3
 
8
- const timersSpeedup = (rate, { apis = ['setTimeout', 'setInterval', 'Date'] } = {}) => {
9
- if (!(typeof rate === 'number' && rate > 0)) throw new TypeError('Expected a positive rate')
10
- const { setTimeout, setInterval, Date: OrigDate } = globalThis
11
- for (const api of apis) {
12
- // eslint-disable-next-line unicorn/prefer-switch
13
- if (api === 'setTimeout') {
14
- globalThis.setTimeout = (fn, ms, ...args) => setTimeout(fn, Math.ceil(ms / rate), ...args)
15
- } else if (api === 'setInterval') {
16
- globalThis.setInterval = (fn, ms, ...args) => setInterval(fn, Math.ceil(ms / rate), ...args)
17
- } else if (api === 'Date') {
18
- const base = OrigDate.now()
19
- globalThis.Date = class Date extends OrigDate {
20
- static now = () => base + Math.floor((OrigDate.now() - base) * rate)
21
- constructor(first = globalThis.Date.now(), ...rest) {
22
- super(first, ...rest)
23
- }
24
- }
25
- } else {
26
- throw new Error(`Unknown or unsupported API in timersSpeedup(): ${api}`)
27
- }
28
- }
29
- }
4
+ import { insideEsbuild } from './dark.cjs'
30
5
 
31
6
  const isBundle = process.env.EXODUS_TEST_ENVIRONMENT === 'bundle' // TODO: improve mocking from bundle
32
- export const exodus = {
7
+
8
+ export const platform = String(process.env.EXODUS_TEST_PLATFORM) // e.g. 'hermes', 'node'
9
+ export const engine = String(process.env.EXODUS_TEST_ENGINE) // e.g. 'hermes:bundle', 'node:bundle', 'node:test', 'node:pure'
10
+ export const implementation = String(node.engine) // aka process.env.EXODUS_TEST_CONTEXT, e.g. 'node:test' or 'pure'
11
+
12
+ /* eslint-disable jsdoc/check-tag-names */
13
+
14
+ /**
15
+ * @experimental API might change
16
+ */
17
+ export const features = {
33
18
  __proto__: null,
34
- platform: String(process.env.EXODUS_TEST_PLATFORM), // e.g. 'hermes', 'node'
35
- engine: String(process.env.EXODUS_TEST_ENGINE), // e.g. 'hermes:bundle', 'node:bundle', 'node:test', 'node:pure'
36
- implementation: String(node.engine), // aka process.env.EXODUS_TEST_CONTEXT, e.g. 'node:test' or 'pure'
37
- features: {
38
- __proto__: null,
39
- timers: Boolean(mock.timers && haveValidTimers),
40
- dynamicRequire: Boolean(!isBundle), // require(non-literal-non-glob), createRequire()(non-builtin)
41
- esmMocks: Boolean(mock.module || isBundle), // support for ESM mocks
42
- esmNamedBuiltinMocks: Boolean(mock.module || isBundle || insideEsbuild()), // support for named ESM imports from builtin module mocks: also fine in --esbuild
43
- esmInterop: Boolean(insideEsbuild() && !isBundle), // loading/using ESM as CJS, ESM mocks creation without a mocker function
44
- concurrency: node.engine !== 'pure', // pure engine doesn't support concurrency
45
- },
46
- mock: {
47
- ...{ timersTrack, timersList, timersDebug, timersAssert, timersSpeedup }, // eslint-disable-line unicorn/no-useless-spread
48
- ...{ fetchRecord, fetchReplay }, // eslint-disable-line unicorn/no-useless-spread
49
- ...{ websocketRecord, websocketReplay }, // eslint-disable-line unicorn/no-useless-spread
50
- },
19
+ dynamicRequire: Boolean(!isBundle), // require(non-literal-non-glob), createRequire()(non-builtin)
20
+ esmMocks: Boolean(mock.module || isBundle), // support for ESM mocks
21
+ esmNamedBuiltinMocks: Boolean(mock.module || isBundle || insideEsbuild()), // support for named ESM imports from builtin module mocks: also fine in --esbuild
22
+ esmInterop: Boolean(insideEsbuild() && !isBundle), // loading/using ESM as CJS, ESM mocks creation without a mocker function
23
+ concurrency: node.engine !== 'pure', // pure engine doesn't support concurrency
51
24
  }
package/src/expect.cjs CHANGED
@@ -13,21 +13,27 @@ function fixupAssertions() {
13
13
 
14
14
  function loadExpect(loadReason) {
15
15
  if (expect) return expect
16
- try {
17
- expect = require('expect').expect
18
- } catch {
19
- throw new Error(`Failed to load 'expect', required for ${loadReason}`)
20
- }
16
+ // eslint-disable-next-line no-undef
17
+ if (typeof EXODUS_TEST_LOAD_EXPECT !== 'undefined' && EXODUS_TEST_LOAD_EXPECT === false) {
18
+ if (loadReason === 'jest.mock') return // allow that and ignore if there is no usage
19
+ throw new Error('FATAL: expect() was optimized out')
20
+ } else {
21
+ try {
22
+ expect = require('expect').expect
23
+ } catch {
24
+ throw new Error(`Failed to load 'expect', required for ${loadReason}`)
25
+ }
21
26
 
22
- // console.log('expect load reason:', loadReason)
23
- try {
24
- expect.extend(require('jest-extended'))
25
- } catch {}
27
+ // console.log('expect load reason:', loadReason)
28
+ try {
29
+ expect.extend(require('jest-extended'))
30
+ } catch {}
26
31
 
27
- for (const x of extend) expect.extend(...x)
28
- for (const [key, value] of set) expect[key] = value
29
- fixupAssertions()
30
- return expect
32
+ for (const x of extend) expect.extend(...x)
33
+ for (const [key, value] of set) expect[key] = value
34
+ fixupAssertions()
35
+ return expect
36
+ }
31
37
  }
32
38
 
33
39
  const areNumeric = (...args) => args.every((a) => typeof a === 'number' || typeof a === 'bigint')
@@ -4,6 +4,7 @@ import { specialEnvironments } from './jest.environment.js'
4
4
 
5
5
  const skipPreset = new Set(['ts-jest'])
6
6
  const EXTS = `.?([cm])[jt]s?(x)` // we differ from jest, allowing [cm] before everything
7
+ const needPreset = ({ preset } = {}) => preset && !skipPreset.has(preset)
7
8
  const normalizeJestConfig = (config) => ({
8
9
  testMatch: [`**/__tests__/**/*${EXTS}`, `**/?(*.)+(spec|test)${EXTS}`],
9
10
  testEnvironment: 'node',
@@ -65,6 +66,67 @@ export const jestConfig = () => {
65
66
 
66
67
  // Methods loadJestConfig() and installJestEnvironment() below are for --jest flag
67
68
 
69
+ // Optimized out in 'bundle' env
70
+ async function loadConfigParts(rawConfig) {
71
+ const presetExtension = /\.([cm]?js|json)$/u
72
+ const suffixes = ['/jest-preset.json', '/jest-preset.js', '/jest-preset.cjs', '/jest-preset.mjs']
73
+ const resolveGlobalSetup = (config, req) => {
74
+ if (config.globalSetup) config.globalSetup = req.resolve(config.globalSetup) // eslint-disable-line @exodus/mutable/no-param-reassign-prop-only
75
+ if (config.globalTeardown) config.globalTeardown = req.resolve(config.globalTeardown) // eslint-disable-line @exodus/mutable/no-param-reassign-prop-only
76
+ }
77
+
78
+ assert(rawConfig.rootDir)
79
+ const { resolve } = await import('node:path')
80
+ const { createRequire } = await import('node:module')
81
+ const { pathToFileURL } = await import('node:url')
82
+ let requireConfig = createRequire(resolve(rawConfig.rootDir, 'package.json'))
83
+ resolveGlobalSetup(rawConfig, requireConfig)
84
+ while (needPreset(rawConfig)) {
85
+ let baseConfig
86
+
87
+ const attemptLoad = async (file) => {
88
+ try {
89
+ const resolved = requireConfig.resolve(file)
90
+ // FIXME: fix linter to allow this
91
+ // const meta = resolved.toLowerCase().endsWith('.json') ? { with: { type: 'json' } } : undefined
92
+ // const presetModule = await import(pathToFileURL(resolved), meta)
93
+ const presetModule = await import(pathToFileURL(resolved))
94
+ requireConfig = createRequire(resolved)
95
+ baseConfig = presetModule.default
96
+ } catch {}
97
+ }
98
+
99
+ // Even if it is relative, it could be a path to module
100
+ for (const suffix of suffixes) {
101
+ if (!baseConfig) await attemptLoad(`${rawConfig.preset}${suffix}`)
102
+ }
103
+
104
+ // If it's a path to a file
105
+ if (!baseConfig && rawConfig.preset[0] === '.' && presetExtension.test(rawConfig.preset)) {
106
+ const { statSync } = await import('node:fs')
107
+ if (statSync(rawConfig.preset).isFile()) await attemptLoad(rawConfig.preset)
108
+ }
109
+
110
+ assert(baseConfig, `Could not load preset: ${rawConfig.preset} `)
111
+ resolveGlobalSetup(baseConfig, requireConfig)
112
+ rawConfig = {
113
+ ...baseConfig,
114
+ ...rawConfig,
115
+ preset: baseConfig.preset,
116
+ setupFiles: [
117
+ ...(baseConfig.setupFiles || []).map((file) => requireConfig.resolve(file)),
118
+ ...(rawConfig.setupFiles || []),
119
+ ],
120
+ setupFilesAfterEnv: [
121
+ ...(baseConfig.setupFilesAfterEnv || []).map((file) => requireConfig.resolve(file)),
122
+ ...(rawConfig.setupFilesAfterEnv || []),
123
+ ],
124
+ }
125
+ }
126
+
127
+ return rawConfig
128
+ }
129
+
68
130
  export async function loadJestConfig(...args) {
69
131
  let rawConfig
70
132
  if (process.env.EXODUS_TEST_JEST_CONFIG === undefined) {
@@ -75,67 +137,12 @@ export async function loadJestConfig(...args) {
75
137
  }
76
138
 
77
139
  const cleanFile = (file) => file.replace(/^<rootDir>\//g, './') // require is already relative to rootDir
78
- const needPreset = ({ preset } = {}) => preset && !skipPreset.has(preset)
79
- const resolveGlobalSetup = (config, req) => {
80
- if (config.globalSetup) config.globalSetup = req.resolve(config.globalSetup) // eslint-disable-line @exodus/mutable/no-param-reassign-prop-only
81
- if (config.globalTeardown) config.globalTeardown = req.resolve(config.globalTeardown) // eslint-disable-line @exodus/mutable/no-param-reassign-prop-only
82
- }
83
-
84
- const presetExtension = /\.([cm]?js|json)$/u
85
- const suffixes = ['/jest-preset.json', '/jest-preset.js', '/jest-preset.cjs', '/jest-preset.mjs']
86
140
  if (needPreset(rawConfig) || rawConfig?.globalSetup || rawConfig?.globalTeardown) {
87
141
  rawConfig.preset = cleanFile(rawConfig.preset) // relative to root dir only at top level, presets shouldn't use <rootDir>
88
142
  if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
89
143
  throw new Error('jest preset and globalSetup/Teardown not yet supported in bundles')
90
144
  } else {
91
- assert(rawConfig.rootDir)
92
- const { resolve } = await import('node:path')
93
- const { createRequire } = await import('node:module')
94
- const { pathToFileURL } = await import('node:url')
95
- let requireConfig = createRequire(resolve(rawConfig.rootDir, 'package.json'))
96
- resolveGlobalSetup(rawConfig, requireConfig)
97
- while (needPreset(rawConfig)) {
98
- let baseConfig
99
-
100
- const attemptLoad = async (file) => {
101
- try {
102
- const resolved = requireConfig.resolve(file)
103
- // FIXME: fix linter to allow this
104
- // const meta = resolved.toLowerCase().endsWith('.json') ? { with: { type: 'json' } } : undefined
105
- // const presetModule = await import(pathToFileURL(resolved), meta)
106
- const presetModule = await import(pathToFileURL(resolved))
107
- requireConfig = createRequire(resolved)
108
- baseConfig = presetModule.default
109
- } catch {}
110
- }
111
-
112
- // Even if it is relative, it could be a path to module
113
- for (const suffix of suffixes) {
114
- if (!baseConfig) await attemptLoad(`${rawConfig.preset}${suffix}`)
115
- }
116
-
117
- // If it's a path to a file
118
- if (!baseConfig && rawConfig.preset[0] === '.' && presetExtension.test(rawConfig.preset)) {
119
- const { statSync } = await import('node:fs')
120
- if (statSync(rawConfig.preset).isFile()) await attemptLoad(rawConfig.preset)
121
- }
122
-
123
- assert(baseConfig, `Could not load preset: ${rawConfig.preset} `)
124
- resolveGlobalSetup(baseConfig, requireConfig)
125
- rawConfig = {
126
- ...baseConfig,
127
- ...rawConfig,
128
- preset: baseConfig.preset,
129
- setupFiles: [
130
- ...(baseConfig.setupFiles || []).map((file) => requireConfig.resolve(file)),
131
- ...(rawConfig.setupFiles || []),
132
- ],
133
- setupFilesAfterEnv: [
134
- ...(baseConfig.setupFilesAfterEnv || []).map((file) => requireConfig.resolve(file)),
135
- ...(rawConfig.setupFilesAfterEnv || []),
136
- ],
137
- }
138
- }
145
+ rawConfig = await loadConfigParts(rawConfig)
139
146
  }
140
147
  }
141
148
 
@@ -148,6 +155,15 @@ export async function loadJestConfig(...args) {
148
155
  return config
149
156
  }
150
157
 
158
+ // Optimized out in 'bundle' env
159
+ async function makeDynamicImport(rootDir) {
160
+ const { resolve } = await import('node:path')
161
+ const { createRequire } = await import('node:module')
162
+ const { pathToFileURL } = await import('node:url')
163
+ const require = createRequire(resolve(rootDir, 'package.json'))
164
+ return (path) => import(pathToFileURL(require.resolve(path))) // does not need json imports
165
+ }
166
+
151
167
  export async function installJestEnvironment(jestGlobals) {
152
168
  const engine = await import('./engine.js')
153
169
 
@@ -173,11 +189,7 @@ export async function installJestEnvironment(jestGlobals) {
173
189
  assert.fail('Requiring non-bundled plugins from bundle is unsupported')
174
190
  }
175
191
  } else if (config.rootDir) {
176
- const { resolve } = await import('node:path')
177
- const { createRequire } = await import('node:module')
178
- const { pathToFileURL } = await import('node:url')
179
- const require = createRequire(resolve(config.rootDir, 'package.json'))
180
- dynamicImport = (path) => import(pathToFileURL(require.resolve(path))) // does not need json imports
192
+ dynamicImport = await makeDynamicImport(config.rootDir)
181
193
  } else {
182
194
  dynamicImport = async () => assert.fail('Unreachable: importing plugins without a rootDir')
183
195
  }
package/src/jest.fn.js CHANGED
@@ -23,6 +23,7 @@ export const jestFunctionMocks = {
23
23
  if (desc?.get && !desc.set && desc.configurable && desc.enumerable) delete obj[name] // e.g. a wrapped module
24
24
  // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
25
25
  obj[name] = fn
26
+ if (Symbol.dispose) fn[Symbol.dispose] = () => fn.mockRestore()
26
27
  return fn
27
28
  },
28
29
  clearAllMocks: applyAllWrap('mockClear'),
package/src/jest.js CHANGED
@@ -6,7 +6,8 @@ import { jestModuleMocks } from './jest.mock.js'
6
6
  import * as jestTimers from './jest.timers.js'
7
7
  import { setupSnapshots } from './jest.snapshot.js'
8
8
  import { createCallerLocationHook } from './dark.cjs'
9
- import { exodus } from './exodus.js'
9
+ import * as exodus from './exodus.js'
10
+ import * as mock from './mock.js'
10
11
  import { expect } from './expect.cjs'
11
12
  import { format as prettyFormat } from './pretty-format.cjs'
12
13
  import { timersTrack, timersDebug } from './timers-track.js'
@@ -247,12 +248,13 @@ if (process.env.EXODUS_TEST_PLATFORM !== 'deno' && globalThis.process) {
247
248
  }
248
249
  }
249
250
 
250
- export const jest = {
251
- exodus: {
251
+ // jest.exodus extension
252
+ function makeJestExodus() {
253
+ return {
252
254
  __proto__: null,
253
255
  ...exodus,
254
256
  mock: {
255
- ...exodus.mock,
257
+ ...mock,
256
258
  fetchNoop: () => {
257
259
  // We can't use pure noop, it will break chained fetch().then(), so let's reject
258
260
  const fetch = () => Promise.reject(new Error('fetch is disabled by mock.fetchNoop()'))
@@ -264,7 +266,15 @@ export const jest = {
264
266
  return globalThis.WebSocket
265
267
  },
266
268
  },
267
- },
269
+ }
270
+ }
271
+
272
+ export const jest = {
273
+ exodus:
274
+ // eslint-disable-next-line no-undef
275
+ typeof EXODUS_TEST_LOAD_JESTEXODUS === 'undefined' || EXODUS_TEST_LOAD_JESTEXODUS !== false
276
+ ? makeJestExodus()
277
+ : undefined,
268
278
  setTimeout: (x) => {
269
279
  assert.equal(typeof x, 'number')
270
280
  defaultTimeout = x
package/src/jest.mock.js CHANGED
@@ -253,7 +253,9 @@ function mockCloneItem(obj, cache) {
253
253
 
254
254
  // TODO: implement for bundles or add a guard against bundles if __mocks__ dir exists
255
255
  let loadMocksDirMock
256
- if (process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle') {
256
+
257
+ // Optimized out in 'bundle' env
258
+ function installMockDirs() {
257
259
  const { existsSync, readdirSync, statSync } = require('node:fs')
258
260
  const { dirname, join, extname } = require('node:path')
259
261
  const dirs = []
@@ -290,7 +292,7 @@ if (process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle') {
290
292
  }
291
293
  }
292
294
 
293
- // Automock does't work on import() in jest anyway, so it's ok to let that require manual jest.mock()
295
+ // Automock does't work on import() in jest anyway, so it's ok to let that require manual jest.mock
294
296
  if (shouldAutoMock.size > 0) {
295
297
  const { Module } = require('node:module')
296
298
  const _require = Module.prototype.require
@@ -305,6 +307,8 @@ if (process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle') {
305
307
  }
306
308
  }
307
309
 
310
+ if (process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle') installMockDirs()
311
+
308
312
  function jestmock(name, mocker, { override = false, actual, builtin, loc } = {}) {
309
313
  // Loaded ESM: isn't mocked
310
314
  // Loaded CJS: mocked via object overriding
@@ -9,8 +9,7 @@ import {
9
9
  import { formatWithAllPlugins } from './pretty-format.cjs'
10
10
  import { jestConfig } from './jest.config.js'
11
11
  import { getTestNamePath } from './dark.cjs'
12
- import { haveSnapshotsReportUnescaped } from './version.js'
13
- import { matchSnapshot, escapeSnapshot } from './engine.pure.snapshot.cjs'
12
+ import { matchSnapshot } from './engine.pure.snapshot.cjs'
14
13
 
15
14
  const { snapshotFormat, snapshotSerializers } = jestConfig()
16
15
  const plugins = []
@@ -140,8 +139,7 @@ const snapOnDisk = (expect, orig, matcherOrSnapshotName, snapshotName) => {
140
139
  wrapContextName(() => context.assert.snapshot(obj), name)
141
140
  } catch (e) {
142
141
  if (typeof e.expected === 'string') {
143
- const escaped = haveSnapshotsReportUnescaped ? e.expected : escapeSnapshot(e.expected)
144
- const final = escaped.includes('\n') ? escaped : `\n${escaped}\n`
142
+ const final = e.expected.includes('\n') ? e.expected : `\n${e.expected}\n`
145
143
  if (final === e.actual) return
146
144
  }
147
145
 
@@ -1,20 +1,8 @@
1
1
  import { mock, assert, awaitForMicrotaskQueue } from './engine.js'
2
2
  import { jestConfig } from './jest.config.js'
3
- import { haveValidTimers, haveNoTimerInfiniteLoopBug } from './version.js'
4
-
5
- const assertHaveTimers = () =>
6
- assert(mock.timers, 'Timer mocking requires Node.js >=20.4.0 || 18 >=18.19.0')
7
-
8
- let timersWarned = false
9
- const warnOldTimers = () => {
10
- if (haveValidTimers || timersWarned) return
11
- timersWarned = true
12
- console.warn('Warning: timer mocks are known to be glitchy before Node.js >=20.11.0')
13
- }
14
3
 
15
4
  let enabled = false
16
5
  const assertEnabledTimers = () => {
17
- assertHaveTimers()
18
6
  assert(enabled, 'You should enable MockTimers first by calling useFakeTimers()')
19
7
  }
20
8
 
@@ -27,15 +15,12 @@ export function useRealTimers() {
27
15
  const doNotFakeDefault = jestConfig().fakeTimers?.doNotFake ?? []
28
16
 
29
17
  export function useFakeTimers({ doNotFake = doNotFakeDefault, ...rest } = {}) {
30
- assertHaveTimers()
31
- warnOldTimers()
32
18
  assert.deepEqual(rest, {}, 'Unsupported options')
33
- const allApis = ['setInterval', 'setTimeout', 'setImmediate']
34
- if (haveValidTimers) allApis.push('Date') // vas not supported in older versions
19
+ const allApis = ['setInterval', 'setTimeout', 'setImmediate', 'Date']
35
20
  for (const name of doNotFake) assert(allApis.includes(name), `Unknown API: ${name}`)
36
21
  const apis = allApis.filter((name) => !doNotFake.includes(name))
37
22
  try {
38
- mock.timers.enable(haveValidTimers ? { apis } : apis) // in older (aka glitchy) versions it's an array
23
+ mock.timers.enable({ apis })
39
24
  } catch (e) {
40
25
  // We allow calling this multiple times and swallow the "MockTimers is already enabled!" error
41
26
  if (e.code !== 'ERR_INVALID_STATE') throw e
@@ -62,7 +47,6 @@ export function runAllTimers() {
62
47
 
63
48
  export function runOnlyPendingTimers() {
64
49
  assertEnabledTimers()
65
- assert(haveNoTimerInfiniteLoopBug, 'runOnlyPendingTimers requires Node.js >=20.11.0')
66
50
  mock.timers.runAll()
67
51
  return this
68
52
  }
package/src/mock.js ADDED
@@ -0,0 +1,25 @@
1
+ export { fetchReplay, websocketRecord, fetchRecord, websocketReplay } from './replay.js'
2
+ export { timersTrack, timersDebug, timersList, timersAssert } from './timers-track.js'
3
+
4
+ export function timersSpeedup(rate, { apis = ['setTimeout', 'setInterval', 'Date'] } = {}) {
5
+ if (!(typeof rate === 'number' && rate > 0)) throw new TypeError('Expected a positive rate')
6
+ const { setTimeout, setInterval, Date: OrigDate } = globalThis
7
+ for (const api of apis) {
8
+ // eslint-disable-next-line unicorn/prefer-switch
9
+ if (api === 'setTimeout') {
10
+ globalThis.setTimeout = (fn, ms, ...args) => setTimeout(fn, Math.ceil(ms / rate), ...args)
11
+ } else if (api === 'setInterval') {
12
+ globalThis.setInterval = (fn, ms, ...args) => setInterval(fn, Math.ceil(ms / rate), ...args)
13
+ } else if (api === 'Date') {
14
+ const base = OrigDate.now()
15
+ globalThis.Date = class Date extends OrigDate {
16
+ static now = () => base + Math.floor((OrigDate.now() - base) * rate)
17
+ constructor(first = globalThis.Date.now(), ...rest) {
18
+ super(first, ...rest)
19
+ }
20
+ }
21
+ } else {
22
+ throw new Error(`Unknown or unsupported API in timersSpeedup(): ${api}`)
23
+ }
24
+ }
25
+ }
package/src/replay.js CHANGED
@@ -3,18 +3,20 @@ const recordingResolver = (type) => (dir, name) => [dir, '__recordings__', type,
3
3
  let replay
4
4
  let readRecordingRaw, writeRecording
5
5
 
6
- if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
7
- replay = require('@exodus/replay') // we can do this in bundle
8
- // eslint-disable-next-line no-undef
9
- const files = EXODUS_TEST_FILES
10
- const baseFile = files.length === 1 ? files[0] : undefined
11
- // eslint-disable-next-line no-undef
12
- const map = typeof EXODUS_TEST_RECORDINGS !== 'undefined' && new Map(EXODUS_TEST_RECORDINGS)
13
- const resolveRecording = (resolver, f) => resolver(f[0], f[1]).join('/')
14
- readRecordingRaw = (resolver) => (baseFile ? map.get(resolveRecording(resolver, baseFile)) : null)
15
- } else {
6
+ function loadReplayBundle() {
7
+ // TODO: also under process.features.require_module
8
+ if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
9
+ replay = require('@exodus/replay') // synchronous
10
+ } else if (!replay) {
11
+ throw new Error('Failed to load @exodus/replay')
12
+ }
13
+ }
14
+
15
+ // Optimized out in 'bundle' env
16
+ async function loadNonBundle() {
17
+ // Preload if synchronous lazy-loading is unavailable
18
+ // TODO: not under process?.features?.require_module
16
19
  try {
17
- // TODO: we can synchronously lazy-load it under process.features.require_module
18
20
  replay = await import('@exodus/replay')
19
21
  } catch {}
20
22
 
@@ -56,8 +58,19 @@ if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
56
58
  }
57
59
  }
58
60
 
61
+ if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
62
+ // eslint-disable-next-line no-undef
63
+ const files = EXODUS_TEST_FILES
64
+ const baseFile = files.length === 1 ? files[0] : undefined
65
+ // eslint-disable-next-line no-undef
66
+ const map = new Map(typeof EXODUS_TEST_RECORDINGS === 'undefined' ? [] : EXODUS_TEST_RECORDINGS)
67
+ const resolveRecording = (resolver, f) => resolver(f[0], f[1]).join('/')
68
+ readRecordingRaw = (resolver) => (baseFile ? map.get(resolveRecording(resolver, baseFile)) : null)
69
+ } else {
70
+ await loadNonBundle()
71
+ }
72
+
59
73
  function readRecording(resolver) {
60
- if (!readRecordingRaw) throw new Error('Replaying recordings is not supported in this engine')
61
74
  const data = readRecordingRaw(resolver)
62
75
  if (typeof data !== 'string') throw new Error('Can not read recording')
63
76
  return JSON.parse(data)
@@ -66,7 +79,7 @@ function readRecording(resolver) {
66
79
  const log = { websocket: undefined, fetch: undefined }
67
80
 
68
81
  export function fetchRecord(options) {
69
- if (!replay) throw new Error('Failed to load @exodus/replay')
82
+ loadReplayBundle()
70
83
  if (log.fetch) throw new Error('Can not record again: already recording or replaying!')
71
84
  if (!writeRecording) throw new Error('Writing fetch log is not supported on this engine')
72
85
  log.fetch = []
@@ -77,7 +90,7 @@ export function fetchRecord(options) {
77
90
  }
78
91
 
79
92
  export function fetchReplay() {
80
- if (!replay) throw new Error('Failed to load @exodus/replay')
93
+ loadReplayBundle()
81
94
  if (log.fetch) throw new Error('Can not replay: already recording or replaying!')
82
95
  log.fetch = readRecording(recordingResolver('fetch')) // Re-initialized from start on each call
83
96
  const fetch = replay.fetchReplayer(log.fetch)
@@ -86,7 +99,7 @@ export function fetchReplay() {
86
99
  }
87
100
 
88
101
  export function websocketRecord(options) {
89
- if (!replay) throw new Error('Failed to load @exodus/replay')
102
+ loadReplayBundle()
90
103
  if (log.websocket) throw new Error('Can not record: already recording or replaying!')
91
104
  if (!writeRecording) throw new Error('Writing WebSocket log is not supported on this engine')
92
105
  log.websocket = []
@@ -97,7 +110,7 @@ export function websocketRecord(options) {
97
110
  }
98
111
 
99
112
  export function websocketReplay(options) {
100
- if (!replay) throw new Error('Failed to load @exodus/replay')
113
+ loadReplayBundle()
101
114
  if (log.websocket) throw new Error('Can not replay: already recording or replaying!')
102
115
  log.websocket = readRecording(recordingResolver('websocket')) // Re-initialized from start on each call
103
116
  const WebSocket = replay.WebSocketReplayer(log.websocket, options)
package/src/version.js CHANGED
@@ -1,21 +1,12 @@
1
1
  import { assert, nodeVersion } from './engine.js'
2
2
 
3
3
  const [major, minor, patch] = nodeVersion.split('.').map(Number)
4
- // older versions are glitchy with before/after on top-level, which is a deal-breaker
5
- // 20.7.0 is fine for node:test but broken with tsx, so we bump to 20.8.0
6
- const ok = (major === 18 && minor >= 19) || (major === 20 && minor >= 8) || major >= 22
7
- assert(ok, 'Node.js version too old or glitchy with node:test, use ^18.19.0 || ^20.8.0 || >=22.0.0')
8
- assert(major !== 22 || minor !== 3, 'Refusing to run on Node.js 22.3.0 specifically, do not use it') // safe-guard
4
+ // Before 20.18, there are no module mocks
5
+ const ok = (major === 20 && minor >= 18) || (major === 22 && minor >= 6) || major > 22
6
+ assert(ok, 'Node.js version too old, use ^20.18.0 || >=22.6.0')
9
7
 
10
8
  export { major, minor, patch }
11
9
 
12
- export const haveModuleMocks =
13
- (major === 20 && minor >= 18) || (major === 22 && minor >= 3) || major > 22
14
- export const haveSnapshots = (major === 22 && minor >= 3) || major > 22
15
- export const haveSnapshotsReportUnescaped = (major === 22 && minor >= 5) || major > 22
16
- export const haveForceExit = (major === 20 && minor > 13) || major >= 22
17
- export const haveValidTimers = (major === 20 && minor >= 11) || major >= 22 // older glitch in various ways / stop executing
18
- export const haveNoTimerInfiniteLoopBug = (major === 20 && minor >= 11) || major >= 22 // mock.timers.runAll() can get into infinite recursion
10
+ // actually 22.3, but prior to 22.5 escaping is wrong. We don't support 22.3-22.5 anyway
11
+ export const haveSnapshots = (major === 22 && minor >= 5) || major > 22
19
12
  export const haveCoverExclude = (major === 22 && minor >= 5) || major > 22
20
- export const haveNetworkInspection =
21
- (major === 20 && minor >= 18) || (major === 22 && minor >= 6) || major > 22