@exodus/test 1.0.0-rc.113 → 1.0.0-rc.115

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
@@ -12,27 +12,28 @@ A runner for `node:test`, `jest`, and `tape` test suites on top of `node:test` (
12
12
 
13
13
  It can run your existing tests on [all runtimes and also browsers](#engines), with snapshots and module mocks:
14
14
 
15
- [![Node.js](https://img.shields.io/badge/Node.js-338750?style=flat-square&logo=Node.js&logoColor=FFF)](https://nodejs.org/api/test.html)
16
- [![Deno](https://img.shields.io/badge/Deno-121417?style=flat-square&logo=Deno&logoColor=FFF)](https://deno.com/)
17
- [![Bun](https://img.shields.io/badge/Bun-F472B6?style=flat-square&logo=Bun&logoColor=FFF)](https://bun.sh/)
18
- [![Electron](https://img.shields.io/badge/Electron-2F3242?style=flat-square&logo=Electron&logoColor=A2ECFB)](http://electronjs.org/)
19
- [![workerd](https://img.shields.io/badge/workerd-F38020?style=flat-square&logo=cloudflareworkers&logoColor=FFF)](https://github.com/cloudflare/workerd)\
20
- [![Chrome](https://img.shields.io/badge/Chrome-4285F4?style=flat-square&logo=GoogleChrome&logoColor=FFF)](https://www.chromium.org/Home/)
21
- [![WebKit](https://img.shields.io/badge/WebKit-006CFF?style=flat-square&logo=Safari&logoColor=FFF)](http://webkit.org/)
22
- [![Firefox](https://img.shields.io/badge/Firefox-FF7139?style=flat-square&logo=Firefox&logoColor=FFF)](https://github.com/mozilla-firefox)
23
- [![Brave](https://img.shields.io/badge/Brave-F0F0F0?style=flat-square&logo=Brave)](https://github.com/brave)
24
- [![Microsoft Edge](https://img.shields.io/badge/Edge-0078D7?style=flat-square)](https://github.com/microsoftedge)
25
- [![Servo](https://img.shields.io/badge/Servo-009D9A?style=flat-square)](https://servo.org/)\
26
- [![Hermes](https://img.shields.io/badge/Hermes-282C34?style=flat-square&logo=React)](https://hermesengine.dev)
27
- [![V8](https://img.shields.io/badge/V8-4285F4?style=flat-square&logo=V8&logoColor=white)](https://v8.dev/docs/d8)
28
- [![JavaScriptCore](https://img.shields.io/badge/JavaScriptCore-006CFF?style=flat-square)](https://docs.webkit.org/Deep%20Dive/JSC/JavaScriptCore.html)
29
- [![SpiderMonkey](https://img.shields.io/badge/SpiderMonkey-FFD681?style=flat-square)](https://spidermonkey.dev/)
30
- [![QuickJS](https://img.shields.io/badge/QuickJS-E58200?style=flat-square)](https://github.com/quickjs-ng/quickjs)
31
- [![XS](https://img.shields.io/badge/XS-0B307A?style=flat-square)](https://github.com/Moddable-OpenSource/moddable-xst)
32
- [![GraalJS](https://img.shields.io/badge/GraalJS-C74634?style=flat-square)](https://github.com/oracle/graaljs)
33
- [![Boa](https://img.shields.io/badge/Boa-F3FF00?style=flat-square)](https://github.com/boa-dev/boa)
34
- [![Escargot](https://img.shields.io/badge/Escargot-1428A0?style=flat-square)](https://github.com/Samsung/escargot)
35
- [![engine262](https://img.shields.io/badge/engine262-f0db4f?style=flat-square&logo=javascript&logoColor=000)](https://github.com/engine262/engine262)
15
+ [![Node.js](https://img.shields.io/badge/Node.js-338750?style=for-the-badge&logo=Node.js&logoColor=FFF)](https://nodejs.org/api/test.html)
16
+ [![Deno](https://img.shields.io/badge/Deno-121417?style=for-the-badge&logo=Deno&logoColor=FFF)](https://deno.com/)
17
+ [![Bun](https://img.shields.io/badge/Bun-F472B6?style=for-the-badge&logo=Bun&logoColor=FFF)](https://bun.sh/)
18
+ [![Electron](https://img.shields.io/badge/Electron-2F3242?style=for-the-badge&logo=Electron&logoColor=A2ECFB)](http://electronjs.org/)
19
+ [![workerd](https://img.shields.io/badge/workerd-F38020?style=for-the-badge&logo=cloudflareworkers&logoColor=FFF)](https://github.com/cloudflare/workerd)\
20
+ [![Chrome](https://img.shields.io/badge/Chrome-4285F4?style=for-the-badge&logo=GoogleChrome&logoColor=FFF)](https://www.chromium.org/Home/)
21
+ [![WebKit](https://img.shields.io/badge/WebKit-006CFF?style=for-the-badge&logo=Safari&logoColor=FFF)](http://webkit.org/)
22
+ [![Firefox](https://img.shields.io/badge/Firefox-FF7139?style=for-the-badge&logo=Firefox&logoColor=FFF)](https://github.com/mozilla-firefox)
23
+ [![Brave](https://img.shields.io/badge/Brave-F0F0F0?style=for-the-badge&logo=Brave)](https://github.com/brave)
24
+ [![Microsoft Edge](https://img.shields.io/badge/Edge-0078D7?style=for-the-badge)](https://github.com/microsoftedge)
25
+ [![Servo](https://img.shields.io/badge/Servo-009D9A?style=for-the-badge)](https://servo.org/)\
26
+ [![Hermes](https://img.shields.io/badge/Hermes-282C34?style=for-the-badge&logo=React)](https://hermesengine.dev)
27
+ [![V8](https://img.shields.io/badge/V8-4285F4?style=for-the-badge&logo=V8&logoColor=white)](https://v8.dev/docs/d8)
28
+ [![JavaScriptCore](https://img.shields.io/badge/JavaScriptCore-006CFF?style=for-the-badge)](https://docs.webkit.org/Deep%20Dive/JSC/JavaScriptCore.html)
29
+ [![SpiderMonkey](https://img.shields.io/badge/SpiderMonkey-FFD681?style=for-the-badge)](https://spidermonkey.dev/)\
30
+ [![QuickJS](https://img.shields.io/badge/QuickJS-E58200?style=for-the-badge)](https://github.com/quickjs-ng/quickjs)
31
+ [![XS](https://img.shields.io/badge/XS-0B307A?style=for-the-badge)](https://github.com/Moddable-OpenSource/moddable)
32
+ [![GraalJS](https://img.shields.io/badge/GraalJS-C74634?style=for-the-badge)](https://github.com/oracle/graaljs)
33
+ [![Boa](https://img.shields.io/badge/Boa-F3FF00?style=for-the-badge)](https://github.com/boa-dev/boa)
34
+ [![Nova](https://img.shields.io/badge/Nova-FF810A?style=for-the-badge)](https://github.com/trynova/nova)
35
+ [![Escargot](https://img.shields.io/badge/Escargot-1428A0?style=for-the-badge&logo=Samsung)](https://github.com/Samsung/escargot)
36
+ [![engine262](https://img.shields.io/badge/engine262-f0db4f?style=for-the-badge&logo=javascript&logoColor=000)](https://github.com/engine262/engine262)
36
37
 
37
38
  Compatible with tests written in:
38
39
 
@@ -44,10 +45,11 @@ See [documentation](https://exodusoss.github.io/test/).
44
45
 
45
46
  ## Features
46
47
 
48
+ - Zero learning curve: runs your existing tests
49
+ - Runs anywhere (including Hermes, the [React Native](https://reactnative.dev/) JavaScript engine)
47
50
  - Native ESM, including in Jest tests
48
51
  - Esbuild on the fly for old faux-ESM interop (enable via `--esbuild`)
49
52
  - TypeScript support
50
- - Runs anywhere (including Hermes, the [React Native](https://reactnative.dev/) JavaScript engine)
51
53
  - Use snapshots to cross-compare between runtimes, browsers and barebones (including Hermes)
52
54
  - Testsuite-agnostic — can run any file as long as it sets exit code based on test results
53
55
  - Built-in [Jest](https://jestjs.io) compatibility (with `--jest`), including `jest.*` global
@@ -65,8 +67,8 @@ See [documentation](https://exodusoss.github.io/test/).
65
67
  - JSDOM env support
66
68
  - Hanging tests error by default (unlike `jest`)
67
69
  - Babel support, picks up your Babel config (enable via `--babel`)
68
- - Unlike `bun:test`, it runs test files in isolated contexts \
69
- Bun leaks globals / side effects between test files ([ref](https://github.com/oven-sh/bun/issues/6024)),
70
+ - Unlike `bun:test`, it runs test files in isolated contexts on Bun. \
71
+ Without this, Bun leaks globals / side effects between test files ([ref](https://github.com/oven-sh/bun/issues/6024)),
70
72
  and has incompatible `test()` lifecycle / order
71
73
 
72
74
  ## Getting started
@@ -174,10 +176,11 @@ Use `--engine` (or `EXODUS_TEST_ENGINE=`) to specify one of:
174
176
  - `hermes:bundle` — [Hermes](https://hermesengine.dev) (React Native JavaScript engine)
175
177
  - `spidermonkey:bundle` — [SpiderMonkey](https://spidermonkey.dev/) (Firefox/Gecko JavaScript engine)
176
178
  - `quickjs:bundle` — [QuickJS](https://github.com/quickjs-ng/quickjs)
177
- - `xs:bundle` — [XS](https://github.com/Moddable-OpenSource/moddable-xst)
179
+ - `xs:bundle` — [Moddable XS](https://github.com/Moddable-OpenSource/moddable)
178
180
  - `graaljs:bundle` — [GraalJS](https://github.com/oracle/graaljs)
179
181
  - `escargot:bundle` — [Escargot](https://github.com/Samsung/escargot)
180
182
  - `boa:bundle` — [Boa](https://github.com/boa-dev/boa)
183
+ - `nova:bundle` — [Nova](https://github.com/trynova/nova) (note that Nova itself is a Work In Progress)
181
184
  - `engine262:bundle` - [engine262](https://github.com/engine262/engine262), the per-spec implementation of ECMA-262
182
185
  (install with [esvu](https://npmjs.com/package/esvu))
183
186
 
package/bin/browsers.js CHANGED
@@ -75,7 +75,13 @@ export async function run(runner, args, { binary, devtools, dropNetwork, timeout
75
75
  const [stdout, stderr] = [[], []]
76
76
 
77
77
  assert(Object.hasOwn(launchers, runner), 'Unexpected runner')
78
- if (!launched[runner]) launched[runner] = launchers[runner]({ binary, devtools: !!devtools })
78
+ try {
79
+ if (!launched[runner]) launched[runner] = launchers[runner]({ binary, devtools: !!devtools })
80
+ } catch {
81
+ // Try a second time, this sometime times out
82
+ if (!launched[runner]) launched[runner] = launchers[runner]({ binary, devtools: !!devtools })
83
+ }
84
+
79
85
  const { page, context } = await newPage(runner, await launched[runner], { binary, dropNetwork })
80
86
 
81
87
  if (throttle) {
@@ -69,6 +69,8 @@ function findBinaryOnce(name) {
69
69
  return require('electron')
70
70
  case 'workerd':
71
71
  return require.resolve('workerd/bin/workerd')
72
+ case 'porffor':
73
+ return require.resolve('porffor/porf')
72
74
  case 'jerryscript':
73
75
  name = 'jerry' // look under this name, including in global
74
76
  return findFile([jsvu, esvu])
package/bin/index.js CHANGED
@@ -39,6 +39,7 @@ const ENGINES = new Map(
39
39
  'deno:test': { binary: 'deno', binaryArgs: denoT, loader: '--preload', ts: 'auto' },
40
40
  'deno:pure': { binary: 'deno', binaryArgs: denoA, pure: true, loader: '--preload', ts: 'auto' },
41
41
  'deno:bundle': { binary: 'deno', binaryArgs: ['run'], target: 'deno1', ...bundleOpts },
42
+ 'workerd:bundle': { binary: 'workerd', binaryArgs: ['test'], ...bundleOpts },
42
43
  // Barebone engines
43
44
  'v8:bundle': { binary: 'd8', binaryArgs: ['--expose-gc'], ...bareboneOpts },
44
45
  'jsc:bundle': { binary: 'jsc', target: 'safari13', ...bareboneOpts },
@@ -55,9 +56,9 @@ const ENGINES = new Map(
55
56
  'boa:bundle': { binary: 'boa', binaryArgs: ['-m'], ...bareboneOpts },
56
57
  'nova:bundle': { binary: 'nova', binaryArgs: ['eval'], ...bareboneOpts },
57
58
  'jerryscript:bundle': { binary: 'jerryscript', ...bareboneOpts },
59
+ 'porffor:bundle': { binary: 'porffor', ...bareboneOpts }, // blocked on https://github.com/CanadaHonk/porffor/issues/176
58
60
  // Special case: running a browser from CLI like a bundle
59
61
  'servo:bundle': { binary: 'servo', binaryArgs: ['--headless'], ...bundleOpts, html: true },
60
- 'workerd:bundle': { binary: 'workerd', binaryArgs: ['test'], ...bundleOpts, workerd: true },
61
62
  // Browser engines
62
63
  'chrome:puppeteer': { binary: 'chrome', browsers: 'puppeteer', ...bundleOpts },
63
64
  'firefox:puppeteer': { binary: 'firefox', browsers: 'puppeteer', ...bundleOpts },
@@ -71,8 +72,8 @@ const ENGINES = new Map(
71
72
  })
72
73
  )
73
74
  const bareOk = ['v8', 'd8', 'spidermonkey', 'quickjs', 'xs', 'hermes', 'shermes']
74
- const bareNotrack = ['jsc', 'escargot', 'boa', 'graaljs', 'jerry', 'engine262', 'servo', 'workerd']
75
- const bareIncomplete = ['ladybird-js', 'nova', 'duk']
75
+ const bareNotrack = ['jsc', 'escargot', 'boa', 'graaljs', 'jerryscript', 'engine262']
76
+ const bareIncomplete = ['ladybird-js', 'nova', 'duktape']
76
77
 
77
78
  const getEnvFlag = (name) => {
78
79
  if (!Object.hasOwn(process.env, name)) return
@@ -662,7 +663,8 @@ async function launch(binary, args, opts = {}, buffering = false) {
662
663
  }
663
664
 
664
665
  const barebones = [...bareOk, ...bareNotrack, ...bareIncomplete]
665
- assertBinary(binary, ['node', 'bun', 'deno', 'electron', 'workerd', ...barebones])
666
+ const bins = ['node', 'bun', 'deno', 'electron', 'workerd', 'servo', 'jerry', 'duk', ...barebones]
667
+ assertBinary(binary, bins)
666
668
  if (binary === c8 && process.platform === 'win32') {
667
669
  ;[binary, args] = ['node', [binary, ...args]]
668
670
  }
@@ -693,7 +695,7 @@ if (options.pure) {
693
695
 
694
696
  setEnv('EXODUS_TEST_CONTEXT', 'pure')
695
697
  const missUnhandled = bareNotrack.includes(options.platform) || isBrowserLike
696
- const isIncomplete = bareIncomplete.includes(options.platform)
698
+ const isIncomplete = bareIncomplete.includes(options.platform) || options.platform === 'workerd'
697
699
  if (missUnhandled) warnHuman(`Warning: ${engineName} does not have unhandled rejections tracking`)
698
700
  if (isIncomplete) warnHuman(`Warning: ${engineName} support is incomplete`)
699
701
 
@@ -707,7 +709,7 @@ if (options.pure) {
707
709
  await writeFile(bundled.fileHtml, `<script src="${bundled.file}"></script>`)
708
710
  }
709
711
 
710
- if (bundled && options.workerd) {
712
+ if (bundled && options.platform === 'workerd') {
711
713
  bundled.fileWrapper = `${bundled.file}.wrapper.js`
712
714
  bundled.fileConfig = `${bundled.file}.capnp`
713
715
  assert(/^[a-z0-9/_.-]+\.js$/iu.test(bundled.file), bundled.file)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/test",
3
- "version": "1.0.0-rc.113",
3
+ "version": "1.0.0-rc.115",
4
4
  "author": "Exodus Movement, Inc.",
5
5
  "description": "A test suite runner",
6
6
  "homepage": "https://github.com/ExodusOSS/test",
@@ -88,6 +88,7 @@
88
88
  "src/engine.js",
89
89
  "src/engine.node.cjs",
90
90
  "src/engine.pure.cjs",
91
+ "src/engine.pure.mock.module.cjs",
91
92
  "src/engine.pure.snapshot.cjs",
92
93
  "src/engine.select.cjs",
93
94
  "src/exodus.js",
@@ -125,22 +126,22 @@
125
126
  "tape.js"
126
127
  ],
127
128
  "scripts": {
128
- "test:_bundle": "EXODUS_TEST_IGNORE='tests/{{jest-extended,inband}/**,jest-when/when.test.*,jest/jest.resetModules.*,jest/mock/jest.mock.mocks-dir.test.js}' npm run test --",
129
+ "test:_bundle": "EXODUS_TEST_IGNORE='tests/{{vendor/jest-extended,inband}/**,vendor/jest-when/when.test.*,jest/jest.resetModules.*,jest/mock/jest.mock.mocks-dir.test.js}' npm run test --",
129
130
  "test": "npm run test:jest --",
130
131
  "test:all": "npm run test:simple && npm run test:jest && npm run test:tape && npm run test:native && npm run test:esbuild && npm run test:pure && npm run test:fetch && npm run test:jsdom && npm run test:bundle",
131
- "test:native": "EXODUS_TEST_IGNORE='{**/typescript/**,**/jest-repo/**/user.test.js}' ./bin/index.js --jest 'tests/**/*.test.{js,cjs,mjs}'",
132
+ "test:native": "EXODUS_TEST_IGNORE='**/jest.transformed/**/*' ./bin/index.js --jest 'tests/**/*.test.{js,cjs,mjs}'",
132
133
  "test:typescript": "node ./bin/index.js --jest --typescript tests/typescript.test.ts",
133
- "test:jest": "node ./bin/index.js --jest --esbuild=ts,user.test.js,sum.test.js",
134
+ "test:jest": "node ./bin/index.js --jest --esbuild=ts,sum.test.js",
134
135
  "test:esbuild": "node ./bin/index.js --jest --esbuild",
135
- "test:tape": "node ./bin/index.js 'tests/tape/tests/*.js' tests/tape.test.js",
136
+ "test:tape": "node ./bin/index.js tests/vendor/tape/test/*.js tests/tape/*.test.*js",
136
137
  "test:simple": "node ./bin/index.js 'tests/*.test.js'",
137
138
  "test:pure": "EXODUS_TEST_ENGINE=node:pure npm run test --",
138
139
  "test:bundle": "EXODUS_TEST_ENGINE=node:bundle npm run test:_bundle --",
139
140
  "test:bun:test": "EXODUS_TEST_ENGINE=bun:test npm run test --",
140
141
  "test:bun:pure": "EXODUS_TEST_ENGINE=bun:pure npm run test --",
141
142
  "test:bun:bundle": "EXODUS_TEST_ENGINE=bun:bundle npm run test:_bundle",
142
- "test:deno:test": "EXODUS_TEST_ENGINE=deno:test node ./bin/index.js tests/tape.test.js tests/simple.test.js tests/env.test.js 'tests/engines/**.test.js' tests/node/simple.test.js tests/node/order.test.js",
143
- "test:deno:pure": "EXODUS_TEST_IGNORE='**/jest-repo/examples/timer/**' EXODUS_TEST_ENGINE=deno:pure npm run test --",
143
+ "test:deno:test": "EXODUS_TEST_ENGINE=deno:test node ./bin/index.js tests/tape/*.test.*js tests/simple.test.js tests/env.test.js 'tests/engines/**.test.js' tests/node/simple.test.js tests/node/order.test.js tests/node/snapshot.test.js tests/node/mock/mock.import.test.js",
144
+ "test:deno:pure": "EXODUS_TEST_IGNORE='**/jest/examples/timer/**' EXODUS_TEST_ENGINE=deno:pure npm run test --",
144
145
  "test:deno:bundle": "EXODUS_TEST_ENGINE=deno:bundle npm run test:_bundle --",
145
146
  "test:electron:node": "EXODUS_TEST_ENGINE=electron-as-node:test npm run test",
146
147
  "test:electron:node:pure": "EXODUS_TEST_ENGINE=electron-as-node:pure npm run test --",
@@ -175,20 +176,20 @@
175
176
  "playwright": "node ./bin/index.js --playwright",
176
177
  "esvu": "esvu",
177
178
  "jsvu": "jsvu",
178
- "jest": "NODE_OPTIONS=--experimental-vm-modules jest tests/jest/ tests/jest-when/",
179
+ "jest": "NODE_OPTIONS=--experimental-vm-modules jest tests/jest/ tests/vendor/jest/ tests/vendor/jest-when/",
179
180
  "lint": "prettier --list-different . && eslint .",
180
181
  "lint:fix": "prettier --write . && eslint --fix ."
181
182
  },
182
183
  "optionalDependencies": {
183
184
  "@chalker/queue": "^1.0.1",
184
- "@exodus/replay": "^1.0.0-rc.10",
185
- "@exodus/test-bundler": "1.0.0-rc.14",
185
+ "@exodus/replay": "^1.0.0-rc.11",
186
+ "@exodus/test-bundler": "1.0.0-rc.16",
186
187
  "c8": "^9.1.0",
187
188
  "expect": "^30.2.0",
188
189
  "fast-glob": "^3.2.11",
189
- "playwright-core": "^1.52.0",
190
+ "playwright-core": "^1.58.2",
190
191
  "pretty-format": "^30.2.0",
191
- "puppeteer-core": "^24.14.0",
192
+ "puppeteer-core": "^24.37.2",
192
193
  "tsx": "^4.21.0"
193
194
  },
194
195
  "devDependencies": {
@@ -1,6 +1,7 @@
1
1
  const assert = require('node:assert/strict')
2
2
  const assertLoose = require('node:assert')
3
3
  const { matchSnapshot } = require('./engine.pure.snapshot.cjs')
4
+ const { mockModule, baseFile, ...mockModuleMethods } = require('./engine.pure.mock.module.cjs')
4
5
 
5
6
  const { setTimeout, setInterval, setImmediate, Date } = globalThis
6
7
  const { clearTimeout, clearInterval, clearImmediate } = globalThis
@@ -30,7 +31,7 @@ const check = (condition, message) => {
30
31
 
31
32
  function parseArgs(args) {
32
33
  check(args.length <= 3)
33
- const name = typeof args[0] === 'string' ? args.shift() : 'test'
34
+ const name = typeof args[0] === 'string' ? args.shift() : undefined
34
35
  const fn = args.pop()
35
36
  const options = args.pop() || {}
36
37
  return { name, options, fn }
@@ -44,8 +45,10 @@ class Context {
44
45
  #fullName
45
46
  #assert
46
47
  #hooks
48
+ #mock
47
49
 
48
50
  constructor(parent, name, options = {}) {
51
+ if (!name || typeof name !== 'string') name = '<anonymous>'
49
52
  Object.assign(this, { root: parent?.root, parent, name, options })
50
53
  this.#fullName = parent && parent !== parent.root ? `${parent.fullName} > ${name}` : name
51
54
  if (this.#fullName === name) this.#fullName = this.#fullName.replace(INBAND_PREFIX_REGEX, '')
@@ -79,6 +82,11 @@ class Context {
79
82
  return this.#assert
80
83
  }
81
84
 
85
+ get mock() {
86
+ if (!this.#mock) this.#mock = new MockTracker()
87
+ return this.#mock
88
+ }
89
+
82
90
  async addHook(type, fn) {
83
91
  if (!this.#hooks) this.#hooks = Object.create(null)
84
92
  if (!this.#hooks[type]) this.#hooks[type] = []
@@ -251,13 +259,18 @@ test.todo = (...args) => {
251
259
  return test(name, { ...options, todo: true }, fn)
252
260
  }
253
261
 
262
+ const codeError = (msg, code) => Object.assign(new Error(msg), { code })
263
+
264
+ let mockTimersEnabled = false
265
+
254
266
  class MockTimers {
255
267
  #enabled = false
256
268
  #base = 0
257
269
  #elapsed = 0
258
270
  #queue = []
259
271
  enable({ now = 0, apis = ['setInterval', 'setTimeout', 'setImmediate', 'Date'] } = {}) {
260
- check(!this.#enabled, 'MockTimers is already enabled!')
272
+ if (mockTimersEnabled) throw codeError('MockTimers is already enabled!', 'ERR_INVALID_STATE')
273
+ mockTimersEnabled = this.#enabled = true
261
274
  this.#base = +now
262
275
  this.#elapsed = 0
263
276
  if (apis.includes('setInterval')) {
@@ -300,7 +313,8 @@ class MockTimers {
300
313
  }
301
314
 
302
315
  reset() {
303
- this.#enabled = false
316
+ if (!this.#enabled) return
317
+ mockTimersEnabled = this.#enabled = false
304
318
  Object.assign(globalThis, { setTimeout, setInterval, setImmediate, Date })
305
319
  Object.assign(globalThis, { clearTimeout, clearInterval, clearImmediate })
306
320
  }
@@ -380,10 +394,18 @@ class MockTimers {
380
394
  }
381
395
  }
382
396
 
383
- const mock = {
384
- module: undefined,
385
- timers: new MockTimers(),
386
- fn: (original = () => {}, implementation = original) => {
397
+ class MockTracker {
398
+ get module() {
399
+ return mockModule
400
+ }
401
+
402
+ #timers
403
+ get timers() {
404
+ if (!this.#timers) this.#timers = new MockTimers()
405
+ return this.#timers
406
+ }
407
+
408
+ fn(original = () => {}, implementation = original) {
387
409
  let impl = implementation
388
410
  const _mock = {
389
411
  calls: [],
@@ -459,20 +481,10 @@ const mock = {
459
481
  return Object.getOwnPropertyDescriptor(target, key)
460
482
  },
461
483
  })
462
- },
463
- }
464
-
465
- if (
466
- process.env.EXODUS_TEST_ENGINE === 'node:pure' ||
467
- process.env.EXODUS_TEST_ENGINE === 'electron-as-node:pure'
468
- ) {
469
- // Try load module mocks from node:test, if present
470
- try {
471
- const nodeTest = require('node:test')
472
- mock.module = nodeTest.mock.module.bind(nodeTest.mock)
473
- } catch {}
484
+ }
474
485
  }
475
486
 
487
+ const mock = new MockTracker()
476
488
  const beforeEach = (fn) => context.addHook('beforeEach', fn)
477
489
  const afterEach = (fn) => context.addHook('afterEach', fn)
478
490
  const before = (fn) => context.addHook('before', fn)
@@ -529,41 +541,24 @@ const awaitForMicrotaskQueue = async () => {
529
541
  for (let i = 0; i < tickPromiseRounds; i++) await promise
530
542
  }
531
543
 
532
- let builtinModules = []
533
- let requireIsRelative = false
534
- let relativeRequire, baseFile, isTopLevelESM, syncBuiltinESMExports, readSnapshot, utilFormat
544
+ let readSnapshot, utilFormat
535
545
  if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
536
- // eslint-disable-next-line no-undef
537
- const files = EXODUS_TEST_FILES
538
- baseFile = files.length === 1 ? files[0] : undefined
539
- isTopLevelESM = () => false
540
546
  // eslint-disable-next-line no-undef
541
547
  const bundleSnaps = typeof EXODUS_TEST_SNAPSHOTS !== 'undefined' && new Map(EXODUS_TEST_SNAPSHOTS)
542
548
  const resolveSnapshot = (f) => snapshotResolver(f[0], f[1]).join('/')
543
549
  readSnapshot = (f = baseFile) => (f && bundleSnaps?.get(resolveSnapshot(f))) || null
544
550
  utilFormat = require('exodus-test:util-format')
545
551
  } else {
546
- const { existsSync, readFileSync } = require('node:fs')
547
- const { dirname, basename, normalize, join } = require('node:path')
548
- const nodeModule = require('node:module')
549
- const files = process.argv.slice(1)
550
- baseFile = files.length === 1 && existsSync(files[0]) ? normalize(files[0]) : undefined
551
- requireIsRelative = Boolean(baseFile)
552
- relativeRequire = baseFile ? nodeModule.createRequire(baseFile) : require
553
- isTopLevelESM = () =>
554
- !baseFile || // assume ESM otherwise
555
- !Object.hasOwn(relativeRequire.cache, baseFile) || // node esm
556
- relativeRequire.cache[baseFile].exports[Symbol.toStringTag] === 'Module' // bun esm
552
+ const { readFileSync } = require('node:fs')
553
+ const { dirname, basename, join } = require('node:path')
557
554
  const resolveSnapshot = (f) => join(...snapshotResolver(dirname(f), basename(f)))
558
555
  readSnapshot = (f = baseFile) => (f ? readFileSync(resolveSnapshot(f), 'utf8') : null)
559
- builtinModules = nodeModule.builtinModules
560
- syncBuiltinESMExports = nodeModule.syncBuiltinESMExports || nodeModule.syncBuiltinExports // bun has it under a different name (also a no-op and always synced atm)
561
556
  utilFormat = require('node:util').format
562
557
  }
563
558
 
564
559
  // eslint-disable-next-line no-undef
565
560
  let snapshotResolver = (dir, name) => [dir, `${name}.snapshot`] // default per Node.js docs
566
- let snapshotSerializers = [(obj) => JSON.stringify(obj, null, 2)]
561
+ let snapshotSerializers = [(obj) => (obj === undefined ? `${obj}` : JSON.stringify(obj, null, 2))]
567
562
  const serializeSnapshot = (obj) => {
568
563
  let val = obj
569
564
  for (const fn of snapshotSerializers) val = fn(val)
@@ -585,9 +580,8 @@ module.exports = {
585
580
  engine: 'pure',
586
581
  ...{ assert, assertLoose },
587
582
  ...{ mock, describe, test, beforeEach, afterEach, before, after },
588
- ...{ builtinModules, syncBuiltinESMExports },
589
583
  ...{ utilFormat, isPromise, nodeVersion, awaitForMicrotaskQueue },
590
- ...{ requireIsRelative, relativeRequire, baseFile, isTopLevelESM, mockModule: mock.module },
584
+ ...{ mockModule, baseFile, ...mockModuleMethods },
591
585
  ...{ readSnapshot, setSnapshotSerializers, setSnapshotResolver },
592
586
  }
593
587
  /* eslint-enable unicorn/no-useless-spread */
@@ -0,0 +1,44 @@
1
+ let mockModule
2
+ let builtinModules = []
3
+ let requireIsRelative = false
4
+ let relativeRequire, baseFile, isTopLevelESM, syncBuiltinESMExports
5
+ if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
6
+ // eslint-disable-next-line no-undef
7
+ const files = EXODUS_TEST_FILES
8
+ baseFile = files.length === 1 ? files[0] : undefined
9
+ isTopLevelESM = () => false
10
+ } else {
11
+ const { existsSync } = require('node:fs')
12
+ const { normalize } = require('node:path')
13
+ const nodeModule = require('node:module')
14
+ const files = process.argv.slice(1)
15
+ baseFile = files.length === 1 && existsSync(files[0]) ? normalize(files[0]) : undefined
16
+ requireIsRelative = Boolean(baseFile)
17
+ relativeRequire = baseFile ? nodeModule.createRequire(baseFile) : require
18
+ isTopLevelESM = () =>
19
+ !baseFile || // assume ESM otherwise
20
+ !Object.hasOwn(relativeRequire.cache, baseFile) || // node esm
21
+ relativeRequire.cache[baseFile].exports[Symbol.toStringTag] === 'Module' // bun esm
22
+ builtinModules = nodeModule.builtinModules
23
+ syncBuiltinESMExports = nodeModule.syncBuiltinESMExports || nodeModule.syncBuiltinExports // bun has it under a different name (also a no-op and always synced atm)
24
+ }
25
+
26
+ if (
27
+ process.env.EXODUS_TEST_ENGINE === 'node:pure' ||
28
+ process.env.EXODUS_TEST_ENGINE === 'electron-as-node:pure'
29
+ ) {
30
+ // Try load module mocks from node:test, if present
31
+ try {
32
+ const nodeTest = require('node:test')
33
+ mockModule = nodeTest.mock.module.bind(nodeTest.mock)
34
+ } catch {}
35
+ } else if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
36
+ globalThis.EXODUS_TEST_MOCK_BUILTINS = new Map()
37
+ }
38
+
39
+ /* eslint-disable unicorn/no-useless-spread */
40
+ module.exports = {
41
+ ...{ mockModule, builtinModules, syncBuiltinESMExports },
42
+ ...{ requireIsRelative, relativeRequire, baseFile, isTopLevelESM },
43
+ }
44
+ /* eslint-enable unicorn/no-useless-spread */
@@ -1,7 +1,8 @@
1
1
  const nameCounts = new Map()
2
2
  let snapshotText, snapshotTextClean
3
3
 
4
- const escapeSnapshot = (str) => str.replaceAll(/([\\`]|\$\{)/gu, '\\$1')
4
+ const escapeSnapshot = (str) => str.replaceAll(/([\\`]|\$\{)/gu, (x) => `\\${x}`)
5
+ const escapeSnapshotKey = (s) => escapeSnapshot(s).replaceAll('\n', '\\n').replaceAll('"', '\\"')
5
6
 
6
7
  function matchSnapshot(readSnapshot, assert, name, serialized) {
7
8
  // We don't have native snapshots, polyfill reading
@@ -25,12 +26,16 @@ function matchSnapshot(readSnapshot, assert, name, serialized) {
25
26
  nameCounts.set(name, count)
26
27
  const escaped = escapeSnapshot(serialized)
27
28
  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
+
34
39
  return assert.fail(`Could not match "${key}" in snapshot. ${addFail}`)
35
40
  }
36
41
 
package/src/expect.cjs CHANGED
@@ -180,3 +180,6 @@ function createExpect() {
180
180
 
181
181
  exports.expect = createExpect()
182
182
  exports.loadExpect = loadExpect
183
+
184
+ // https://github.com/trynova/nova/issues/935
185
+ if (process.env.EXODUS_TEST_PLATFORM === 'nova') exports.expect = require('expect').expect
package/src/jest.mock.js CHANGED
@@ -47,7 +47,6 @@ export const jestModuleMocks = {
47
47
  jestModuleMocks.dontMock = jestModuleMocks.unmock
48
48
 
49
49
  if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
50
- globalThis.EXODUS_TEST_MOCK_BUILTINS = new Map()
51
50
  Object.assign(jestModuleMocks, {
52
51
  __mockBundle(name, builtin, actual, mock) {
53
52
  jestmock(name, mock, { actual, builtin, override: true })