@exodus/test 1.0.0-rc.74 → 1.0.0-rc.76

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/bin/index.js CHANGED
@@ -229,7 +229,6 @@ function parseOptions() {
229
229
 
230
230
  const isTTY = process.stdout.isTTY
231
231
  const isCI = process.env.CI
232
- const { options, patterns } = parseOptions()
233
232
  const warnHuman = isTTY && !isCI ? (...args) => console.warn(...args) : () => {}
234
233
  if (isCI) process.env.FORCE_COLOR = '1' // should support colored output even though not a TTY, overridable with --no-color
235
234
 
@@ -239,6 +238,11 @@ const setEnv = (name, value) => {
239
238
  process.env[name] = value === undefined ? '' : value
240
239
  }
241
240
 
241
+ const { options, patterns } = parseOptions()
242
+ if (!process.env.FORCE_COLOR && process.stdout.hasColors?.() && process.stderr.hasColors?.()) {
243
+ setEnv('FORCE_COLOR', '1') // Default to color output for subprocesses if our stream supports it
244
+ }
245
+
242
246
  const engineName = `${options.engine} engine` // used for warnings to user
243
247
  const engineOptions = ENGINES.get(options.engine)
244
248
  assert(engineOptions, `Unknown engine: ${options.engine}`)
@@ -565,7 +569,7 @@ async function launch(binary, args, opts = {}, buffering = false) {
565
569
  if (options.pure) {
566
570
  setEnv('EXODUS_TEST_CONTEXT', 'pure')
567
571
  warnHuman(`${engineName} is experimental and may not work an expected`)
568
- const missUnhandled = ['jsc', 'quickjs'].includes(options.platform) || options.browsers
572
+ const missUnhandled = ['jsc'].includes(options.platform) || options.browsers
569
573
  if (missUnhandled) warnHuman(`Warning: ${engineName} does not have unhandled rejections tracking`)
570
574
 
571
575
  const runOne = async (inputFile) => {
@@ -1,2 +1,8 @@
1
1
  // A slimmed-down version of globals.cjs specifically for Node.js bundle
2
2
  Object.assign(process, { argv: process.argv })
3
+
4
+ if (!globalThis.crypto) {
5
+ // Old Node.js, we polyfill it as our bundler polyfills crypto module using webcrypto RNG
6
+ const r = require // prevent embed
7
+ globalThis.crypto = r('node:crypto').webcrypto
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/test",
3
- "version": "1.0.0-rc.74",
3
+ "version": "1.0.0-rc.76",
4
4
  "author": "Exodus Movement, Inc.",
5
5
  "description": "A test suite runner",
6
6
  "homepage": "https://github.com/ExodusMovement/test",
@@ -100,26 +100,28 @@
100
100
  "CHANGELOG.md"
101
101
  ],
102
102
  "scripts": {
103
- "test:_pure": "EXODUS_TEST_IGNORE='tests/jest-extended/**' npm run test",
104
103
  "test:_bundle": "EXODUS_TEST_IGNORE='tests/{{jest-extended,inband}/**,jest-when/when.test.*,jest/jest.resetModules.*}' npm run test --",
105
104
  "test": "npm run test:jest --",
106
- "test:all": "npm run test:jest && npm run test:tape && npm run test:native && npm run test:pure && npm run test:typescript && npm run test:fetch && npm run test:jsdom",
105
+ "test:all": "npm run test:jest && npm run test:tape && npm run test:native && npm run test:pure && npm run test:typescript && npm run test:fetch && npm run test:jsdom && npm run test:bundle",
107
106
  "test:native": "EXODUS_TEST_IGNORE='{**/typescript/**,**/jest-repo/**/user.test.js}' ./bin/index.js --jest 'tests/**/*.test.{js,cjs,mjs}'",
108
107
  "test:typescript": "./bin/index.js --jest --typescript tests/typescript.test.ts",
109
108
  "test:jest": "./bin/index.js --jest --esbuild",
110
- "test:tape": "./bin/index.js --esbuild 'tests/tape/tests/*.js' tests/tape.test.js",
111
- "test:pure": "EXODUS_TEST_ENGINE=node:pure npm run test:_pure",
112
- "test:bun": "EXODUS_TEST_ENGINE=bun:pure npm run test:_pure",
109
+ "test:tape": "./bin/index.js 'tests/tape/tests/*.js' tests/tape.test.js",
110
+ "test:pure": "EXODUS_TEST_ENGINE=node:pure npm run test --",
111
+ "test:bundle": "EXODUS_TEST_ENGINE=node:bundle npm run test:_bundle --",
112
+ "test:bun:pure": "EXODUS_TEST_ENGINE=bun:pure npm run test --",
113
+ "test:bun:bundle": "EXODUS_TEST_ENGINE=bun:bundle npm run test:_bundle",
113
114
  "test:deno": "EXODUS_TEST_ENGINE=deno:bundle npm run test:_bundle --",
114
115
  "test:electron": "EXODUS_TEST_ENGINE=electron-as-node:test npm run test",
115
- "test:electron:pure": "EXODUS_TEST_ENGINE=electron-as-node:pure npm run test:_pure",
116
+ "test:electron:pure": "EXODUS_TEST_ENGINE=electron-as-node:pure npm run test --",
116
117
  "test:electron:bundle": "EXODUS_TEST_ENGINE=electron-as-node:bundle npm run test:_bundle",
117
118
  "test:chrome:puppeteer": "EXODUS_TEST_ENGINE=chrome:puppeteer npm run test:_bundle --",
118
119
  "test:firefox:puppeteer": "EXODUS_TEST_ENGINE=firefox:puppeteer npm run test:_bundle --",
119
120
  "test:chromium:playwright": "EXODUS_TEST_ENGINE=chromium:playwright npm run test:_bundle --",
120
121
  "test:firefox:playwright": "EXODUS_TEST_ENGINE=firefox:playwright npm run test:_bundle --",
121
122
  "test:webkit:playwright": "EXODUS_TEST_ENGINE=webkit:playwright npm run test:_bundle --",
122
- "test:bundle": "EXODUS_TEST_ENGINE=node:bundle npm run test:_bundle --",
123
+ "test:v8": "npm run test:d8 --",
124
+ "test:javascriptcore": "npm run test:jsc --",
123
125
  "test:d8": "EXODUS_TEST_ENGINE=d8:bundle npm run test:_bundle --",
124
126
  "test:jsc": "EXODUS_TEST_ENGINE=jsc:bundle npm run test:_bundle --",
125
127
  "test:hermes": "EXODUS_TEST_ENGINE=hermes:bundle npm run test:_bundle --",
@@ -129,6 +131,7 @@
129
131
  "test:fetch": "./bin/index.js --jest --drop-network --engine node:pure tests/fetch.test.js tests/websocket.test.js",
130
132
  "test:jsdom": "EXODUS_TEST_JEST_CONFIG='{\"testMatch\":[\"**/*.jsdom-test.js\"],\"testEnvironment\":\"jsdom\", \"rootDir\": \".\"}' ./bin/index.js --jest",
131
133
  "coverage": "./bin/index.js --jest --esbuild --coverage",
134
+ "playwright": "./bin/index.js --playwright",
132
135
  "jsvu": "jsvu",
133
136
  "jest": "NODE_OPTIONS=--experimental-vm-modules jest tests/jest/ tests/jest-when/",
134
137
  "lint": "prettier --list-different . && eslint .",
@@ -178,7 +181,7 @@
178
181
  "@jest/globals": "^29.7.0",
179
182
  "@types/jest-when": "^3.5.2",
180
183
  "@typescript-eslint/eslint-plugin": "^7.15.0",
181
- "electron": "^35.1.4",
184
+ "electron": "^35.2.2",
182
185
  "eslint": "^8.44.0",
183
186
  "jest": "^29.7.0",
184
187
  "jest-matcher-utils": "^29.7.0",
@@ -308,19 +308,23 @@ class MockTimers {
308
308
  }
309
309
 
310
310
  #microtick() {
311
- const next =
312
- this.#queue.find((x) => x.runAt === -1) || // immediates are first
313
- this.#queue.find((x) => x.runAt <= this.#elapsed)
311
+ const next = this.#queue.find((x) => x.runAt <= this.#elapsed) // sorted
314
312
  if (!next) return null
315
- if (next.interval === undefined) {
316
- this.#queue = this.#queue.filter((x) => x !== next)
317
- } else {
313
+ this.#queue = this.#queue.filter((x) => x !== next)
314
+ if (next.interval !== undefined) {
318
315
  next.runAt += next.interval
316
+ this.#schedule(next)
319
317
  }
320
318
 
321
319
  next.callback(...next.args)
322
320
  }
323
321
 
322
+ #schedule(entry) {
323
+ const before = this.#queue.findIndex((x) => x.runAt > entry.runAt)
324
+ if (before === -1) return this.#queue.push(entry)
325
+ this.#queue.splice(before, 0, entry)
326
+ }
327
+
324
328
  runAll() {
325
329
  this.tick(Math.max(0, ...this.#queue.map((x) => x.runAt - this.#elapsed)))
326
330
  }
@@ -331,19 +335,19 @@ class MockTimers {
331
335
 
332
336
  #setTimeout(callback, delay, ...args) {
333
337
  const id = { callback, runAt: delay + this.#elapsed, args }
334
- this.#queue.push(id)
338
+ this.#schedule(id)
335
339
  return id
336
340
  }
337
341
 
338
342
  #setInterval(callback, delay, ...args) {
339
343
  const id = { callback, runAt: delay + this.#elapsed, interval: delay, args }
340
- this.#queue.push(id)
344
+ this.#schedule(id)
341
345
  return id
342
346
  }
343
347
 
344
348
  #setImmediate(callback, ...args) {
345
349
  const id = { callback, runAt: -1, args }
346
- this.#queue.push(id)
350
+ this.#schedule(id)
347
351
  return id
348
352
  }
349
353
 
@@ -462,6 +466,7 @@ const isPromise = (x) => Boolean(x && x.then && x.catch && x.finally)
462
466
  const nodeVersion = '9999.99.99'
463
467
  const awaitForMicrotaskQueue = async () => {
464
468
  if (globalThis?.process?.nextTick) {
469
+ if (globalThis.Bun) await Promise.resolve() // No idea what's up with Bun microtasks
465
470
  // We are in microtasks, awaiting for "next" tick will get us out of here
466
471
  return new Promise((resolve) => globalThis.process.nextTick(resolve))
467
472
  }
package/src/exodus.js CHANGED
@@ -15,8 +15,8 @@ export const exodus = {
15
15
  timers: Boolean(mock.timers && haveValidTimers),
16
16
  dynamicRequire: Boolean(!isBundle), // require(non-literal-non-glob), createRequire()(non-builtin)
17
17
  esmMocks: Boolean(mock.module || isBundle), // support for ESM mocks
18
+ esmNamedBuiltinMocks: Boolean(mock.module || isBundle || insideEsbuild), // support for named ESM imports from builtin module mocks: also fine in --esbuild
18
19
  esmInterop: Boolean(insideEsbuild && !isBundle), // loading/using ESM as CJS, ESM mocks creation without a mocker function
19
- esmNamedBuiltinMocks: Boolean(mock.module || insideEsbuild || isBundle), // support for named ESM imports from builtin module mocks
20
20
  concurrency: node.engine !== 'pure', // pure engine doesn't support concurrency
21
21
  },
22
22
  mock: {
package/src/expect.cjs CHANGED
@@ -11,9 +11,11 @@ function fixupAssertions() {
11
11
  assertionsDelta = 0
12
12
  }
13
13
 
14
- function loadExpect() {
14
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
+ function loadExpect(loadReason) {
15
16
  if (expect) return expect
16
17
  expect = require('expect').expect
18
+ // console.log('expect load reason:', loadReason)
17
19
  const matchers = require('jest-extended')
18
20
  expect.extend(matchers)
19
21
  for (const x of extend) expect.extend(...x)
@@ -26,7 +28,7 @@ const areNumeric = (...args) => args.every((a) => typeof a === 'number' || typeo
26
28
 
27
29
  const matchers = {
28
30
  __proto__: null,
29
- toBe: (x, y) => x === y,
31
+ toBe: (x, y) => Object.is(x, y),
30
32
  toBeNull: (x) => x === null,
31
33
  toBeTruthy: (x) => x,
32
34
  toBeFalsy: (x) => !x,
@@ -53,42 +55,76 @@ const matchers = {
53
55
 
54
56
  const matchersFalseNegative = {
55
57
  __proto__: null,
56
- toEqual: (x, y) => x === y,
57
- toStrictEqual: (x, y) => x === y,
58
+ toEqual: (x, y) => Object.is(x, y),
59
+ toStrictEqual: (x, y) => Object.is(x, y),
58
60
  toContain: (x, c) => Array.isArray(x) && [...x].includes(c),
59
61
  toBeEven: (x) => Number.isSafeInteger(x) && x % 2 === 0,
60
62
  toBeOdd: (x) => Number.isSafeInteger(x) && x % 2 === 1,
61
63
  }
62
64
 
65
+ const doesNotThrow = (x) => {
66
+ try {
67
+ x()
68
+ return [true]
69
+ } catch (err) {
70
+ return [false, err]
71
+ }
72
+ }
73
+
63
74
  function createExpect() {
64
75
  return new Proxy(() => {}, {
65
76
  apply: (target, that, [x, ...rest]) => {
66
- if (rest.length > 0) return loadExpect()(x, ...rest)
77
+ if (rest.length > 0) return loadExpect('rest')(x, ...rest)
67
78
  return new Proxy(Object.create(null), {
68
79
  get: (_, name) => {
69
80
  const matcher = matchers[name] || matchersFalseNegative[name]
70
- if (matcher)
81
+ if (matcher) {
82
+ return (...args) => {
83
+ if (!matcher(x, ...args)) return loadExpect(`.${name} fail`)(x)[name](...args)
84
+ assertionsDelta++
85
+ }
86
+ }
87
+
88
+ if (name === 'toThrow') {
71
89
  return (...args) => {
72
- if (!matcher(x, ...args)) return loadExpect()(x)[name](...args)
90
+ if (args.length > 0) return loadExpect('.toThrow args')(x)[name](...args)
91
+ const [passed] = doesNotThrow(x)
92
+ if (passed) return loadExpect('.toThrow fail')(() => {})[name](...args)
73
93
  assertionsDelta++
74
94
  }
95
+ }
75
96
 
76
97
  if (name === 'not')
77
98
  return new Proxy(Object.create(null), {
78
99
  get: (_, not) => {
79
- if (matchers[not])
100
+ if (not === 'toThrow') {
80
101
  return (...args) => {
81
- if (matchers[not](x, ...args)) return loadExpect()(x).not[not](...args)
102
+ const [passed, err] = doesNotThrow(x)
103
+ if (!passed) {
104
+ return loadExpect('.not.toThrow fail')(() => {
105
+ throw err
106
+ }).not.toThrow(...args)
107
+ }
108
+
109
+ assertionsDelta++
110
+ }
111
+ }
112
+
113
+ if (matchers[not]) {
114
+ return (...args) => {
115
+ if (matchers[not](x, ...args)) {
116
+ return loadExpect(`.not.${not} fail`)(x).not[not](...args)
117
+ }
118
+
82
119
  assertionsDelta++
83
120
  }
121
+ }
84
122
 
85
- // console.log ({ loadReason: 'not', name: not })
86
- return loadExpect()(x).not[not]
123
+ return loadExpect(`.not.${not}`)(x).not[not]
87
124
  },
88
125
  })
89
126
 
90
- // console.log ({ loadReason: 'expect', name })
91
- return loadExpect()(x)[name]
127
+ return loadExpect(`.${name}`)(x)[name]
92
128
  },
93
129
  })
94
130
  },
@@ -106,8 +142,7 @@ function createExpect() {
106
142
  }
107
143
  }
108
144
 
109
- // console.log({ loadReason: 'get', name })
110
- return loadExpect()[name]
145
+ return loadExpect(`get ${name}`)[name]
111
146
  },
112
147
  set: (_, name, value) => {
113
148
  if (expect) {
package/src/jest.js CHANGED
@@ -46,14 +46,22 @@ function parseArgs(list, targs) {
46
46
  return result
47
47
  }
48
48
 
49
+ // Hack for common testing with simple arrow functions
50
+ const formatArg = (x) => {
51
+ if (x && x instanceof Function) {
52
+ if (`${x}` === '()=>{}') return '() => {}' // likely minified by esbuild
53
+ if (globalThis.Bun && `${x}`.replace(/\s/g, '') === '()=>{}') return '() => {}' // Bun breaks formatting
54
+ }
55
+
56
+ return x
57
+ }
58
+
49
59
  const eachCallerLocation = []
50
60
  const makeEach =
51
61
  (impl) =>
52
62
  (list, ...rest) =>
53
63
  (template, fn, ...restArgs) => {
54
64
  eachCallerLocation.unshift(getCallerLocation())
55
- // Hack for common testing with simple arrow functions, until we can disable esbuild minification
56
- const formatArg = (x) => (x && x instanceof Function && `${x}` === '()=>{}' ? '() => {}' : x)
57
65
  // better than nothing
58
66
  const printed = (x) =>
59
67
  x && [null, Array.prototype, Object.prototype].includes(Object.getPrototypeOf(x))
package/src/jest.mock.js CHANGED
@@ -190,7 +190,13 @@ function mockCloneItem(obj, cache) {
190
190
  for (let c = obj; c && c !== Object.prototype; c = Object.getPrototypeOf(c)) stack.unshift(c)
191
191
  let modified = stack.length > 1
192
192
  for (const level of stack) {
193
- for (const [name, desc] of Object.entries(Object.getOwnPropertyDescriptors(level))) {
193
+ const descriptors = Object.getOwnPropertyDescriptors(level)
194
+ const entries = Object.entries(descriptors)
195
+ for (const sym of [Symbol.toStringTag]) {
196
+ if (sym && Object.hasOwn(descriptors, sym)) entries.push([sym, descriptors[sym]]) // Missed by Object.entries
197
+ }
198
+
199
+ for (const [name, desc] of entries) {
194
200
  if (name === 'constructor') continue
195
201
 
196
202
  for (const key of ['get', 'set', 'value']) {
@@ -255,7 +261,7 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
255
261
  const value = mocker ? expand(mocker()) : mockClone(mapActual.get(resolved))
256
262
  mapMocks.set(resolved, value)
257
263
 
258
- loadExpect() // we need to do this as we don't want mocks affecting expect
264
+ loadExpect('jest.mock') // we need to do this as we don't want mocks affecting expect
259
265
 
260
266
  if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
261
267
  if (builtin) globalThis.EXODUS_TEST_MOCK_BUILTINS.set(builtin, value)
@@ -274,7 +280,7 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
274
280
 
275
281
  const topLevelESM = isTopLevelESM()
276
282
  let likelyESM = topLevelESM && !insideEsbuild && ![null, resolved].includes(resolveImport(name))
277
- let okFromESM = false
283
+ let isOverridenBuiltinSynchedWithESM = false
278
284
  const isBuiltIn = builtinModules.includes(resolved)
279
285
  const isNodeCache = (x) => x && x.id && x.path && x.filename && x.children && x.paths && x.loaded
280
286
  if (isBuiltIn && !isNodeCache(require.cache[resolved])) {
@@ -288,7 +294,7 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
288
294
  overrideModule(resolved, true) // Override builtin modules
289
295
  if (syncBuiltinESMExports) {
290
296
  syncBuiltinESMExports()
291
- okFromESM = true
297
+ isOverridenBuiltinSynchedWithESM = true
292
298
  }
293
299
  }
294
300
 
@@ -310,15 +316,17 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
310
316
  likelyESM = true
311
317
  }
312
318
 
313
- if (likelyESM || (!okFromESM && topLevelESM)) {
319
+ const mocksNodeVersionNote = 'mocks are available only on Node.js >=20.18 <21 || >=22.3'
320
+ if (likelyESM || (!isOverridenBuiltinSynchedWithESM && topLevelESM)) {
314
321
  // Native module mocks is required if loading ESM or __from__ ESM
315
322
  // No good way to check the locations that import the module, but we can check top-level file
316
323
  // Built-in modules are fine though
317
- assert(mock.module, 'ESM module mocks are available only on Node.js >=22.3')
324
+ assert(mock.module, `ESM module ${mocksNodeVersionNote}`)
318
325
  } else if (isBuiltIn && name.startsWith('node:') && !override) {
319
- assert(mock.module, 'Native non-overriding node:* mocks are available only on Node.js >=22.3')
326
+ assert(mock.module, `Native non-overriding node:* ${mocksNodeVersionNote}`)
320
327
  }
321
328
 
329
+ if (value[Symbol.toStringTag] === 'Module') value.__esModule = true
322
330
  const obj = { defaultExport: value }
323
331
  if (isBuiltIn && isObject(value)) obj.namedExports = value
324
332
  if (insideEsbuild) {
@@ -12,8 +12,15 @@ const warnOldTimers = () => {
12
12
  console.warn('Warning: timer mocks are known to be glitchy before Node.js >=20.11.0')
13
13
  }
14
14
 
15
+ let enabled = false
16
+ const assertEnabledTimers = () => {
17
+ assertHaveTimers()
18
+ assert(enabled, 'You should enable MockTimers first by calling useFakeTimers()')
19
+ }
20
+
15
21
  export function useRealTimers() {
16
22
  mock.timers?.reset()
23
+ enabled = false
17
24
  return this
18
25
  }
19
26
 
@@ -41,26 +48,39 @@ export function useFakeTimers({ doNotFake = doNotFakeDefault, ...rest } = {}) {
41
48
  globalThis[name] = (id) => id && fn(id)
42
49
  }
43
50
 
51
+ enabled = true
44
52
  return this
45
53
  }
46
54
 
47
55
  export function runAllTimers() {
48
- assertHaveTimers()
49
- warnOldTimers()
50
- mock.timers.tick(100_000_000_000) // > 3 years
56
+ assertEnabledTimers()
57
+ advanceTimersByTime(100_000_000_000) // > 3 years
51
58
  return this
52
59
  }
53
60
 
54
61
  export function runOnlyPendingTimers() {
62
+ assertEnabledTimers()
55
63
  assert(haveNoTimerInfiniteLoopBug, 'runOnlyPendingTimers requires Node.js >=20.11.0')
56
64
  mock.timers.runAll()
57
65
  return this
58
66
  }
59
67
 
60
68
  export function advanceTimersByTime(time) {
61
- assertHaveTimers()
62
- warnOldTimers()
63
- mock.timers.tick(time)
69
+ assert(Number.isSafeInteger(time) && time > 0)
70
+ assertEnabledTimers()
71
+ // We split this into multiple steps to run timers scheduled during the time we are running
72
+ const minSteps = Math.min(1000, time) // usually just split e.g. 5 seconds into 1000 * 5ms
73
+ const step = Number(Math.floor(time / minSteps).toPrecision(1))
74
+ const steps = Math.floor(time / step) // up to 2x higher than minSteps
75
+ const last = time - steps * step
76
+ // 1999 -> { step: 1, steps: 1999, last: 0 }
77
+ // 2001 -> { step: 2, steps: 1000, last: 1 }
78
+ for (let i = 0; i < steps; i++) {
79
+ if (!enabled) break // got disabled while looping
80
+ mock.timers.tick(step)
81
+ }
82
+
83
+ if (last > 0 && enabled) mock.timers.tick(last)
64
84
  return this
65
85
  }
66
86
 
@@ -79,14 +99,13 @@ export async function runOnlyPendingTimersAsync() {
79
99
  }
80
100
 
81
101
  export async function advanceTimersByTimeAsync(time) {
82
- assertHaveTimers()
83
- warnOldTimers()
84
-
102
+ assertEnabledTimers()
85
103
  if (mock.timers.tickAsync) {
86
104
  await mock.timers.tickAsync(time)
87
105
  } else {
88
106
  for (let i = 0; i < time; i++) {
89
107
  await awaitForMicrotaskQueue()
108
+ if (!enabled) break // got disabled while looping
90
109
  mock.timers.tick(1)
91
110
  }
92
111
  }
@@ -96,6 +115,7 @@ export async function advanceTimersByTimeAsync(time) {
96
115
  }
97
116
 
98
117
  export function setSystemTime(time) {
118
+ assertEnabledTimers()
99
119
  mock.timers.setTime(+time)
100
120
  return this
101
121
  }
package/src/version.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { assert, nodeVersion } from './engine.js'
2
2
 
3
3
  const [major, minor, patch] = nodeVersion.split('.').map(Number)
4
- assert(major !== 21, 'Node.js 21.x is deprecated!') // reached EOL, no reason to even test
5
4
  // older versions are glitchy with before/after on top-level, which is a deal-breaker
6
5
  // 20.7.0 is fine for node:test but broken with tsx, so we bump to 20.8.0
7
6
  const ok = (major === 18 && minor >= 19) || (major === 20 && minor >= 8) || major >= 22
@@ -10,7 +9,8 @@ assert(major !== 22 || minor !== 3, 'Refusing to run on Node.js 22.3.0 specifica
10
9
 
11
10
  export { major, minor, patch }
12
11
 
13
- export const haveModuleMocks = (major === 22 && minor >= 3) || major > 22
12
+ export const haveModuleMocks =
13
+ (major === 20 && minor >= 18) || (major === 22 && minor >= 3) || major > 22
14
14
  export const haveSnapshots = (major === 22 && minor >= 3) || major > 22
15
15
  export const haveSnapshotsReportUnescaped = (major === 22 && minor >= 5) || major > 22
16
16
  export const haveForceExit = (major === 20 && minor > 13) || major >= 22