@exodus/test 1.0.0-rc.93 → 1.0.0-rc.95

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.
Files changed (41) hide show
  1. package/bin/browsers.js +9 -0
  2. package/bin/electron.js +5 -2
  3. package/bin/inband.js +2 -1
  4. package/bin/index.js +72 -35
  5. package/bin/reporter.js +3 -2
  6. package/loaders/babel.cjs +0 -2
  7. package/package.json +25 -63
  8. package/src/engine.js +2 -2
  9. package/src/engine.node.cjs +6 -1
  10. package/src/engine.pure.cjs +39 -7
  11. package/src/engine.pure.snapshot.cjs +5 -3
  12. package/src/jest.config.fs.js +2 -1
  13. package/src/jest.config.js +66 -6
  14. package/src/jest.js +3 -3
  15. package/src/jest.mock.js +10 -9
  16. package/src/version.js +1 -0
  17. package/bundler/babel-worker.cjs +0 -62
  18. package/bundler/bundle.js +0 -548
  19. package/bundler/modules/ansi-styles.cjs +0 -49
  20. package/bundler/modules/assert-strict.cjs +0 -1
  21. package/bundler/modules/child_process.cjs +0 -10
  22. package/bundler/modules/cluster.cjs +0 -27
  23. package/bundler/modules/crypto.cjs +0 -5
  24. package/bundler/modules/empty/function-throw.cjs +0 -4
  25. package/bundler/modules/empty/module-throw.cjs +0 -1
  26. package/bundler/modules/fs-promises.cjs +0 -1
  27. package/bundler/modules/fs.cjs +0 -123
  28. package/bundler/modules/globals.cjs +0 -340
  29. package/bundler/modules/globals.node.cjs +0 -8
  30. package/bundler/modules/http.cjs +0 -119
  31. package/bundler/modules/https.cjs +0 -11
  32. package/bundler/modules/jest-message-util.js +0 -5
  33. package/bundler/modules/jest-util.js +0 -22
  34. package/bundler/modules/module.cjs +0 -16
  35. package/bundler/modules/node-buffer.cjs +0 -3
  36. package/bundler/modules/text-encoding-utf.cjs +0 -90
  37. package/bundler/modules/tty.cjs +0 -10
  38. package/bundler/modules/url.cjs +0 -32
  39. package/bundler/modules/util-format.cjs +0 -48
  40. package/bundler/modules/util.cjs +0 -4
  41. package/bundler/modules/ws.cjs +0 -20
package/bin/browsers.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import assert from 'node:assert/strict'
2
+ import { spawnSync } from 'node:child_process'
2
3
  import { readFile } from 'node:fs/promises'
4
+ import { dirname, resolve } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
3
6
  import { findBinary } from './find-binary.js'
4
7
 
5
8
  // See https://playwright.dev/docs/browsers
@@ -126,3 +129,9 @@ export async function run(runner, args, { binary, devtools, dropNetwork, timeout
126
129
  await context.close()
127
130
  }
128
131
  }
132
+
133
+ export function runPlaywrightCommand(args) {
134
+ const playwright = dirname(fileURLToPath(import.meta.resolve('playwright-core/package.json')))
135
+ const cli = resolve(playwright, 'cli.js')
136
+ return spawnSync(cli, args, { stdio: 'inherit' })
137
+ }
package/bin/electron.js CHANGED
@@ -27,11 +27,14 @@ protocol.registerSchemesAsPrivileged([
27
27
  },
28
28
  ])
29
29
 
30
+ const enableIntegration = process.env.EXODUS_TEST_ENGINE === 'electron:pure'
30
31
  const devtools = process.env.EXODUS_TEST_DEVTOOLS === '1'
31
32
  const preload = fileURLToPath(import.meta.resolve('./electron.preload.cjs'))
32
33
  const partition = 'tmp' // not persistent
33
- const staticPreferences = { sandbox: true, contextIsolation: true, spellcheck: false }
34
- const webPreferences = { ...staticPreferences, partition, preload }
34
+ const securityPreferences = enableIntegration
35
+ ? { sandbox: false, contextIsolation: false, nodeIntegration: true }
36
+ : { sandbox: true, contextIsolation: true, preload }
37
+ const webPreferences = { ...securityPreferences, partition, spellcheck: false }
35
38
  const html = '<!doctype html><html><body></body></html>'
36
39
  const headers = { 'content-type': 'text/html' }
37
40
 
package/bin/inband.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { resolve } from 'node:path'
2
+ import { pathToFileURL } from 'node:url'
2
3
  import { describe, after } from '../src/engine.js'
3
4
 
4
5
  const files = JSON.parse(process.env.EXODUS_TEST_INBAND)
@@ -6,7 +7,7 @@ if (!Array.isArray(files)) throw new Error('Unexpected')
6
7
 
7
8
  for (const file of files.sort()) {
8
9
  await describe(`EXODUS_TEST_INBAND:${file}`, async () => {
9
- await import(resolve(file))
10
+ await import(pathToFileURL(resolve(file)))
10
11
  })
11
12
  }
12
13
 
package/bin/index.js CHANGED
@@ -1,25 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn, spawnSync, execFile as execFileCallback } from 'node:child_process'
3
+ import { spawn, execFile as execFileCallback } from 'node:child_process'
4
4
  import { promisify } from 'node:util'
5
5
  import { once } from 'node:events'
6
6
  import { fileURLToPath } from 'node:url'
7
- import { basename, dirname, resolve, join } from 'node:path'
8
- import { createRequire } from 'node:module'
7
+ import { basename, join } from 'node:path'
9
8
  import { randomUUID } from 'node:crypto'
10
9
  import { existsSync, rmSync, realpathSync } from 'node:fs'
11
10
  import { unlink } from 'node:fs/promises'
12
11
  import { tmpdir, availableParallelism, homedir } from 'node:os'
13
12
  import assert from 'node:assert/strict'
14
13
  // The following make sense only when we run the code in the same Node.js version, i.e. engineOptions.haveIsOk
15
- import { haveModuleMocks, haveSnapshots, haveForceExit } from '../src/version.js'
14
+ import { haveModuleMocks, haveSnapshots, haveForceExit, haveCoverExclude } from '../src/version.js'
16
15
  import { findBinary } from './find-binary.js'
17
16
  import * as browsers from './browsers.js'
18
17
  import { glob as globImplementation } from '../src/glob.cjs'
19
18
 
20
- const bindir = dirname(fileURLToPath(import.meta.url))
21
19
  const DEFAULT_PATTERNS = [`**/?(*.)+(spec|test).?([cm])[jt]s?(x)`] // do not trust magic dirs by default
22
-
23
20
  const bundleOpts = { pure: true, bundle: true, esbuild: true, ts: 'auto' }
24
21
  const bareboneOpts = { ...bundleOpts, barebone: true }
25
22
  const hermesAv = ['-Og', '-Xmicrotask-queue']
@@ -61,7 +58,7 @@ const barebonesOk = ['d8', 'spidermonkey', 'quickjs', 'xs', 'hermes']
61
58
  const barebonesUnhandled = ['jsc', 'escargot', 'graaljs', 'engine262']
62
59
 
63
60
  const getEnvFlag = (name) => {
64
- if (!Object.hasOwn(process.env, name)) return false
61
+ if (!Object.hasOwn(process.env, name)) return
65
62
  if ([undefined, '', '0', '1'].includes(process.env[name])) return process.env[name] === '1'
66
63
  throw new Error(`Unexpected ${name} env value, expected '', '0', or '1'`)
67
64
  }
@@ -80,7 +77,7 @@ function parseOptions() {
80
77
  esbuild: false,
81
78
  babel: false,
82
79
  coverage: getEnvFlag('EXODUS_TEST_COVERAGE'),
83
- coverageEngine: 'c8', // c8 or node
80
+ coverageEngine: process.platform === 'win32' ? 'node' : 'c8', // c8 or node. TODO: can we use c8 on win?
84
81
  watch: false,
85
82
  only: false,
86
83
  passWithNoTests: false,
@@ -100,8 +97,8 @@ function parseOptions() {
100
97
  const args = [...process.argv]
101
98
 
102
99
  // First argument should be node
103
- assert.equal(basename(args.shift()), 'node')
104
- assert.equal(basename(process.argv0), 'node')
100
+ assert(['node', 'node.exe'].includes(basename(args.shift())))
101
+ assert(['node', 'node.exe'].includes(basename(process.argv0)))
105
102
 
106
103
  // Second argument should be this script
107
104
  const jsname = args.shift()
@@ -109,9 +106,7 @@ function parseOptions() {
109
106
  assert(basename(jsname) === 'exodus-test' || pathsEqual(jsname, fileURLToPath(import.meta.url)))
110
107
 
111
108
  if (args[0] === '--playwright') {
112
- const playwright = dirname(fileURLToPath(import.meta.resolve('playwright-core/package.json')))
113
- const cli = resolve(playwright, 'cli.js')
114
- const res = spawnSync(cli, args.slice(1), { stdio: 'inherit' })
109
+ const res = browsers.runPlaywrightCommand(args.slice(1))
115
110
  process.exitCode = res.status ?? 1
116
111
  process.exit(0)
117
112
  }
@@ -165,6 +160,9 @@ function parseOptions() {
165
160
  case '--coverage':
166
161
  options.coverage = true
167
162
  break
163
+ case '--no-coverage':
164
+ options.coverage = false
165
+ break
168
166
  case '--watch':
169
167
  options.watch = true
170
168
  break
@@ -280,10 +278,6 @@ setEnv('EXODUS_TEST_ENVIRONMENT', options.bundle ? 'bundle' : '') // perhaps swi
280
278
  assert(!options.devtools || isBrowserLike || !options.pure, engineFlagError('devtools'))
281
279
  assert(!options.throttle || options.browsers, engineFlagError('throttle-cpu'))
282
280
 
283
- const require = createRequire(import.meta.url)
284
- const resolveRequire = (query) => require.resolve(query)
285
- const resolveImport = import.meta.resolve && ((query) => fileURLToPath(import.meta.resolve(query)))
286
-
287
281
  const args = []
288
282
 
289
283
  if (haveModuleMocks && engineOptions.haveIsOk) {
@@ -303,7 +297,7 @@ if (options.pure) {
303
297
  assert(!options.watch, `Can not use --watch with with ${engineName}`)
304
298
  assert(options.testNamePattern.length === 0, '--test-name-pattern requires node:test engine now')
305
299
  } else if (options.engine === 'node:test' || options.engine === 'electron-as-node:test') {
306
- const reporter = resolveRequire('./reporter.js')
300
+ const reporter = import.meta.resolve('./reporter.js')
307
301
  args.push('--test', '--no-warnings=ExperimentalWarning', '--test-reporter', reporter)
308
302
 
309
303
  if (haveSnapshots && engineOptions.haveIsOk) args.push('--experimental-test-snapshots')
@@ -339,6 +333,7 @@ if (process.env.EXODUS_TEST_IGNORE) {
339
333
  // The comment below is disabled, we don't auto-mock @jest/globals anymore, and having our loader first is faster
340
334
  // [Disabled] Our loader should be last, as enabling module mocks confuses other loaders
341
335
  let jestConfig = null
336
+ let globalTeardown
342
337
  if (options.jest) {
343
338
  const { loadJestConfig } = await import('../src/jest.config.js')
344
339
  const config = await loadJestConfig(process.cwd())
@@ -346,7 +341,7 @@ if (options.jest) {
346
341
  if (options.bundle) {
347
342
  setEnv('EXODUS_TEST_JEST_CONFIG', JSON.stringify(jestConfig))
348
343
  } else {
349
- args.push(options.hasImportLoader ? '--import' : '-r', resolve(bindir, 'jest.js'))
344
+ args.push(options.hasImportLoader ? '--import' : '-r', import.meta.resolve('./jest.js'))
350
345
  }
351
346
 
352
347
  if (config.testFailureExitCode !== undefined) {
@@ -379,14 +374,47 @@ if (options.jest) {
379
374
  return !ignoreRegexes.some((r) => r.test(resolved))
380
375
  }
381
376
  }
377
+
378
+ if (config.collectCoverage && options.coverage === undefined) options.coverage = true
379
+ if (config.maxWorkers && options.concurrency === undefined) {
380
+ options.concurrency = config.maxWorkers
381
+ }
382
+
383
+ for (const key of ['globalSetup', 'globalTeardown']) {
384
+ if (!config[key]) continue
385
+ const { default: method } = await import(config[key])
386
+ assert(method, `config.${key} does not export a default method`)
387
+ assert(method.length === 0, `Arguments for config.${key} are not supported yet`)
388
+ if (key === 'globalTeardown') {
389
+ globalTeardown = method
390
+ } else {
391
+ await method() // globalSetup
392
+ }
393
+ }
394
+ }
395
+
396
+ if (options.concurrency) {
397
+ const raw = options.concurrency
398
+ let concurrency = raw
399
+ if (typeof raw === 'string') {
400
+ if (/^\d{1,15}%$/u.test(raw)) {
401
+ const perc = Number(raw.slice(0, -1))
402
+ concurrency = Math.max(1, Math.round((perc * availableParallelism()) / 100))
403
+ } else {
404
+ assert(/^\d{1,15}$/u.test(raw), `Wrong concurrency: ${raw}`)
405
+ concurrency = Number(raw)
406
+ }
407
+ }
408
+
409
+ assert(Number.isSafeInteger(concurrency) && concurrency >= 1, `Wrong concurrency: ${raw}`)
410
+ options.concurrency = concurrency
382
411
  }
383
412
 
384
413
  if (options.esbuild && !options.bundle) {
385
- assert(resolveImport)
386
414
  setEnv('EXODUS_TEST_ESBUILD', options.esbuild)
387
415
  if (options.hasImportLoader) {
388
416
  const optional = options.esbuild === '*' ? '' : '.optional'
389
- args.push('--import', resolveImport(`../loaders/esbuild${optional}.js`))
417
+ args.push('--import', import.meta.resolve(`../loaders/esbuild${optional}.js`))
390
418
  } else if (options.flagEngine === false) {
391
419
  // Engine is set via env, --esbuild set via flag. Allow but warn
392
420
  console.warn(`Warning: ${engineName} does not support --esbuild option`)
@@ -398,7 +426,7 @@ if (options.esbuild && !options.bundle) {
398
426
 
399
427
  if (options.babel) {
400
428
  assert(!options.esbuild, 'Options --babel and --esbuild are mutually exclusive')
401
- args.push('-r', resolveRequire('../loaders/babel.cjs'))
429
+ args.push('-r', import.meta.resolve('../loaders/babel.cjs'))
402
430
  }
403
431
 
404
432
  if (options.typescript) {
@@ -406,10 +434,9 @@ if (options.typescript) {
406
434
  assert(!options.babel, 'Options --typescript and --babel are mutually exclusive')
407
435
 
408
436
  if (options.ts === 'flag') {
409
- assert(resolveImport)
410
437
  assert(options.hasImportLoader)
411
438
  // TODO: switch to native --experimental-strip-types where available
412
- args.push('--import', resolveImport('../loaders/typescript.js'))
439
+ args.push('--import', import.meta.resolve('../loaders/typescript.js'))
413
440
  } else if (options.ts !== 'auto') {
414
441
  throw new Error(`Processing --typescript is not possible with ${engineName}`)
415
442
  }
@@ -509,9 +536,15 @@ if (options.coverage) {
509
536
  assert.equal(options.binary, 'node', 'Coverage is only supported with Node.js')
510
537
  if (options.coverageEngine === 'node') {
511
538
  args.push('--experimental-test-coverage')
539
+ if (haveCoverExclude && engineOptions.haveIsOk) {
540
+ args.push(
541
+ `--test-coverage-exclude=**/@exodus/test/src/**`,
542
+ `--test-coverage-exclude=${DEFAULT_PATTERNS[0]}`
543
+ )
544
+ }
512
545
  } else if (options.coverageEngine === 'c8') {
513
546
  c8 = findBinary('c8')
514
- if (resolveImport) assert.equal(c8, resolveImport('c8/bin/c8.js'))
547
+ assert.equal(c8, fileURLToPath(import.meta.resolve('c8/bin/c8.js')))
515
548
  args.unshift(options.binary)
516
549
  options.binary = c8
517
550
  // perhaps use text-summary ?
@@ -524,7 +557,7 @@ if (options.coverage) {
524
557
  if (options.binary === 'electron') {
525
558
  if (isBrowserLike) {
526
559
  assert(!options.binaryArgs)
527
- options.binaryArgs = [resolveImport('./electron.js')]
560
+ options.binaryArgs = [fileURLToPath(import.meta.resolve('./electron.js'))]
528
561
  } else {
529
562
  setEnv('ELECTRON_RUN_AS_NODE', '1')
530
563
  }
@@ -587,6 +620,10 @@ async function launch(binary, args, opts = {}, buffering = false) {
587
620
 
588
621
  const barebones = [...barebonesOk, ...barebonesUnhandled]
589
622
  assertBinary(binary, ['node', 'bun', 'deno', 'electron', ...barebones, 'v8']) // v8 is an alias to d8
623
+ if (binary === c8 && process.platform === 'win32') {
624
+ ;[binary, args] = ['node', [binary, ...args]]
625
+ }
626
+
590
627
  if (options.dropNetwork) {
591
628
  switch (process.platform) {
592
629
  case 'darwin':
@@ -618,6 +655,8 @@ if (options.pure) {
618
655
  const file = buildFile ? bundled.file : inputFile
619
656
  if (bundled?.errors.length > 0) return { ok: false, output: bundled.errors }
620
657
 
658
+ const failedBare = 'EXODUS_TEST_FAILED_EXIT_CODE_1'
659
+ const cleanOut = (out) => out.replaceAll(`\n${failedBare}\n`, '\n').replaceAll(failedBare, '')
621
660
  const { binaryArgs = [] } = options
622
661
  // Timeout is fallback if timeout in script hangs, 50x as it can be adjusted per-script inside them
623
662
  // Do we want to extract timeouts from script code instead? Also, hermes might be slower, so makes sense to increase
@@ -627,23 +666,19 @@ if (options.pure) {
627
666
  const fullArgs = [...binaryArgs, ...args, file]
628
667
  const { code = 0, stdout, stderr } = await launch(options.binary, fullArgs, { timeout }, true)
629
668
  const ms = Number(process.hrtime.bigint() - start) / 1e6
630
- const failedBare = 'EXODUS_TEST_FAILED_EXIT_CODE_1'
631
- if (stdout.includes(failedBare)) {
632
- const stdoutClean = stdout.replaceAll(`\n${failedBare}\n`, '\n').replaceAll(failedBare, '')
633
- return { ok: false, output: [stdoutClean, stderr], ms }
634
- }
635
-
669
+ if (stdout.includes(failedBare)) return { ok: false, output: [cleanOut(stdout), stderr], ms }
636
670
  const ok = code === 0 && !/^(✖ FAIL|‼ FATAL) /mu.test(stdout)
637
671
  return { ok, output: [stdout, stderr], ms }
638
672
  } catch (err) {
639
673
  const retryOnXS = new Set(['SIGSEGV', 'SIGBUS'])
640
- if (options.engine === 'xs:bundle' && retryOnXS.has(err.signal) && attempt < 2) {
641
- // xs sometimes randomly crashes with SIGSEGV on CI. Allow 3 attempts (allow 0 - 1 to fail)
674
+ if (options.engine === 'xs:bundle' && retryOnXS.has(err.signal) && attempt < 4) {
675
+ // xs sometimes randomly crashes with SIGSEGV on CI. Allow 5 attempts (allow #0 - #3 to fail)
642
676
  return runOne(inputFile, attempt + 1)
643
677
  }
644
678
 
645
679
  const ms = Number(process.hrtime.bigint() - start) / 1e6
646
- const { code, stdout = '', stderr = '', signal, killed } = err
680
+ const { code, stderr = '', signal, killed } = err
681
+ const stdout = cleanOut(err.stdout || '')
647
682
  if (code === null) {
648
683
  assert(signal)
649
684
  const message = ` ${signal}${killed ? ' (killed)' : ''}`
@@ -710,3 +745,5 @@ if (options.pure) {
710
745
  const { code } = await launch(options.binary, [...args, ...files])
711
746
  process.exitCode = code
712
747
  }
748
+
749
+ if (globalTeardown) await globalTeardown()
package/bin/reporter.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { inspect } from 'node:util'
3
- import { relative, resolve } from 'node:path'
3
+ import { relative, resolve, normalize } from 'node:path'
4
4
  import { spec as SpecReporter } from 'node:test/reporters'
5
5
  import { fileURLToPath } from 'node:url'
6
6
  import { color, haveColors, dim } from './color.js'
@@ -134,7 +134,7 @@ export default async function nodeTestReporterExodus(source) {
134
134
  switch (type) {
135
135
  case 'test:dequeue':
136
136
  if (data.nesting === 0 && data.name?.startsWith?.(INBAND_PREFIX)) {
137
- files.add(data.name.slice(INBAND_PREFIX.length))
137
+ files.add(normalize(data.name.slice(INBAND_PREFIX.length)))
138
138
  } else if (data.nesting === 0 && !Object.hasOwn(data, 'file')) {
139
139
  files.add(relative(cwd, data.name)) // old-style
140
140
  } else if (isTopLevelTest(data)) {
@@ -154,6 +154,7 @@ export default async function nodeTestReporterExodus(source) {
154
154
  assert(path.pop() === data.name)
155
155
  break
156
156
  case 'test:fail':
157
+ if (!process.exitCode) process.exitCode = 1 // node:test might not set this on errors in describe()
157
158
  if (!pskip(path)) print(`${color('✖ FAIL ', 'red')}${pathstr(path)}${formatSuffix(data)}`)
158
159
  if (path.length > 0) assert(path.pop() === data.name) // afterAll can generate failures too, with an empty path
159
160
  if (!data.todo) failedFiles.add(file)
package/loaders/babel.cjs CHANGED
@@ -2,7 +2,5 @@ const register = require('@babel/register')
2
2
 
3
3
  register({
4
4
  compact: false,
5
- babelrc: false,
6
- plugins: ['@babel/plugin-transform-modules-commonjs'],
7
5
  ignore: [], // do not ignore node_modules
8
6
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/test",
3
- "version": "1.0.0-rc.93",
3
+ "version": "1.0.0-rc.95",
4
4
  "author": "Exodus Movement, Inc.",
5
5
  "description": "A test suite runner",
6
6
  "homepage": "https://github.com/ExodusMovement/test",
@@ -45,31 +45,6 @@
45
45
  "bin/inband.js",
46
46
  "bin/jest.js",
47
47
  "bin/reporter.js",
48
- "bundler/babel-worker.cjs",
49
- "bundler/bundle.js",
50
- "bundler/modules/empty/function-throw.cjs",
51
- "bundler/modules/empty/module-throw.cjs",
52
- "bundler/modules/ansi-styles.cjs",
53
- "bundler/modules/assert-strict.cjs",
54
- "bundler/modules/child_process.cjs",
55
- "bundler/modules/cluster.cjs",
56
- "bundler/modules/crypto.cjs",
57
- "bundler/modules/fs.cjs",
58
- "bundler/modules/fs-promises.cjs",
59
- "bundler/modules/http.cjs",
60
- "bundler/modules/https.cjs",
61
- "bundler/modules/globals.cjs",
62
- "bundler/modules/globals.node.cjs",
63
- "bundler/modules/jest-message-util.js",
64
- "bundler/modules/jest-util.js",
65
- "bundler/modules/module.cjs",
66
- "bundler/modules/node-buffer.cjs",
67
- "bundler/modules/text-encoding-utf.cjs",
68
- "bundler/modules/tty.cjs",
69
- "bundler/modules/url.cjs",
70
- "bundler/modules/util.cjs",
71
- "bundler/modules/util-format.cjs",
72
- "bundler/modules/ws.cjs",
73
48
  "loaders/babel.cjs",
74
49
  "loaders/esbuild.js",
75
50
  "loaders/esbuild.optional.js",
@@ -108,12 +83,13 @@
108
83
  "scripts": {
109
84
  "test:_bundle": "EXODUS_TEST_IGNORE='tests/{{jest-extended,inband}/**,jest-when/when.test.*,jest/jest.resetModules.*}' npm run test --",
110
85
  "test": "npm run test:jest --",
111
- "test:all": "npm run test:jest && npm run test:tape && npm run test:native && npm run test:esbuild && npm run test:pure && npm run test:typescript && npm run test:fetch && npm run test:jsdom && npm run test:bundle",
86
+ "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:typescript && npm run test:fetch && npm run test:jsdom && npm run test:bundle",
112
87
  "test:native": "EXODUS_TEST_IGNORE='{**/typescript/**,**/jest-repo/**/user.test.js}' ./bin/index.js --jest 'tests/**/*.test.{js,cjs,mjs}'",
113
- "test:typescript": "./bin/index.js --jest --typescript tests/typescript.test.ts",
114
- "test:jest": "./bin/index.js --jest --esbuild=ts,user.test.js,sum.test.js",
115
- "test:esbuild": "./bin/index.js --jest --esbuild",
116
- "test:tape": "./bin/index.js 'tests/tape/tests/*.js' tests/tape.test.js",
88
+ "test:typescript": "node ./bin/index.js --jest --typescript tests/typescript.test.ts",
89
+ "test:jest": "node ./bin/index.js --jest --esbuild=ts,user.test.js,sum.test.js",
90
+ "test:esbuild": "node ./bin/index.js --jest --esbuild",
91
+ "test:tape": "node ./bin/index.js 'tests/tape/tests/*.js' tests/tape.test.js",
92
+ "test:simple": "node ./bin/index.js 'tests/*.test.js'",
117
93
  "test:pure": "EXODUS_TEST_ENGINE=node:pure npm run test --",
118
94
  "test:bundle": "EXODUS_TEST_ENGINE=node:bundle npm run test:_bundle --",
119
95
  "test:bun:pure": "EXODUS_TEST_ENGINE=bun:pure npm run test --",
@@ -139,52 +115,28 @@
139
115
  "test:xs": "EXODUS_TEST_ENGINE=xs:bundle npm run test:_bundle --",
140
116
  "test:graaljs": "EXODUS_TEST_ENGINE=graaljs:bundle npm run test:_bundle --",
141
117
  "test:escargot": "EXODUS_TEST_ENGINE=escargot:bundle npm run test:_bundle --",
142
- "test:fetch": "./bin/index.js --jest --drop-network --engine node:pure tests/fetch.test.js tests/websocket.test.js",
118
+ "test:fetch": "node ./bin/index.js --jest --drop-network --engine node:pure 'tests/replay/*.test.js'",
143
119
  "test:jsdom": "EXODUS_TEST_JEST_CONFIG='{\"testMatch\":[\"**/*.jsdom-test.js\"],\"testEnvironment\":\"jsdom\", \"rootDir\": \".\"}' ./bin/index.js --jest",
144
- "coverage": "./bin/index.js --jest --esbuild --coverage",
145
- "playwright": "./bin/index.js --playwright",
120
+ "coverage": "node ./bin/index.js --jest --esbuild --coverage",
121
+ "playwright": "node ./bin/index.js --playwright",
146
122
  "jsvu": "jsvu",
147
123
  "jest": "NODE_OPTIONS=--experimental-vm-modules jest tests/jest/ tests/jest-when/",
148
124
  "lint": "prettier --list-different . && eslint .",
149
125
  "lint:fix": "prettier --write . && eslint --fix ."
150
126
  },
151
127
  "optionalDependencies": {
152
- "@babel/core": "^7.0.0",
153
- "@babel/plugin-syntax-import-attributes": "^7.0.0",
154
- "@babel/plugin-syntax-typescript": "^7.0.0",
155
- "@babel/plugin-transform-block-scoping": "^7.0.0",
156
- "@babel/plugin-transform-class-properties": "^7.0.0",
157
- "@babel/plugin-transform-classes": "^7.0.0",
158
- "@babel/plugin-transform-private-methods": "^7.0.0",
159
- "@babel/register": "^7.0.0",
160
128
  "@chalker/queue": "^1.0.1",
161
129
  "@exodus/replay": "^1.0.0-rc.9",
162
- "@ungap/url-search-params": "^0.2.2",
130
+ "@exodus/test-bundler": "1.0.0-rc.1",
163
131
  "amaro": "^0.0.5",
164
- "assert": "^2.1.0",
165
- "browserify-zlib": "^0.2.0",
166
- "buffer": "^6.0.3",
167
132
  "c8": "^9.1.0",
168
- "constants-browserify": "^1.0.0",
169
- "crypto-browserify": "^3.12.0",
170
- "esbuild": "~0.25.4",
171
- "events": "^3.3.0",
172
133
  "expect": "^29.7.0",
173
134
  "fast-glob": "^3.2.11",
174
- "flow-remove-types": "^2.242.0",
175
135
  "jest-extended": "^4.0.2",
176
- "jsdom": "^24.1.0",
177
- "os-browserify": "^0.3.0",
178
- "path-browserify": "^1.0.1",
179
136
  "playwright-core": "^1.52.0",
180
137
  "pretty-format": "^29.7.0",
181
138
  "puppeteer-core": "^24.6.0",
182
- "querystring-es3": "^0.2.1",
183
- "stream-browserify": "^3.0.0",
184
- "timers-browserify": "^2.0.12",
185
- "tsx": "^4.19.4",
186
- "url": "^0.11.0",
187
- "util": "^0.12.5"
139
+ "tsx": "^4.19.4"
188
140
  },
189
141
  "devDependencies": {
190
142
  "@exodus/eslint-config": "^5.24.0",
@@ -198,15 +150,25 @@
198
150
  "jest-matcher-utils": "^29.7.0",
199
151
  "jest-serializer-ansi-escapes": "^3.0.0",
200
152
  "jest-when": "^3.6.0",
201
- "jsvu": "^3.0.0"
153
+ "jsdom": "^26.1.0",
154
+ "jsvu": "^3.0.0",
155
+ "prettier": "^3.0.3"
202
156
  },
203
157
  "peerDependencies": {
204
- "electron": "*"
158
+ "@babel/register": "^7.0.0",
159
+ "electron": "*",
160
+ "jsdom": "*"
205
161
  },
206
162
  "peerDependenciesMeta": {
163
+ "@babel/register": {
164
+ "optional": true
165
+ },
207
166
  "electron": {
208
167
  "optional": true
168
+ },
169
+ "jsdom": {
170
+ "optional": true
209
171
  }
210
172
  },
211
- "packageManager": "pnpm@9.4.0+sha1.9217c800d4ab947a7aee520242a7b70d64fc7638"
173
+ "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
212
174
  }
package/src/engine.js CHANGED
@@ -15,8 +15,8 @@ export { builtinModules, syncBuiltinESMExports }
15
15
  const { utilFormat, isPromise, nodeVersion, awaitForMicrotaskQueue } = engine
16
16
  export { utilFormat, isPromise, nodeVersion, awaitForMicrotaskQueue }
17
17
 
18
- const { requireIsRelative, relativeRequire, isTopLevelESM } = engine
19
- export { requireIsRelative, relativeRequire, isTopLevelESM }
18
+ const { requireIsRelative, relativeRequire, isTopLevelESM, mockModule } = engine
19
+ export { requireIsRelative, relativeRequire, isTopLevelESM, mockModule }
20
20
 
21
21
  const { readSnapshot, setSnapshotSerializers, setSnapshotResolver } = engine
22
22
  export { readSnapshot, setSnapshotSerializers, setSnapshotResolver }
@@ -3,6 +3,7 @@ const assertLoose = require('node:assert')
3
3
  const { types, format: utilFormat } = require('node:util')
4
4
  const { existsSync, readFileSync } = require('node:fs')
5
5
  const { normalize, basename, dirname, join: pathJoin } = require('node:path')
6
+ const { pathToFileURL } = require('node:url')
6
7
  const { createRequire, builtinModules, syncBuiltinESMExports } = require('node:module')
7
8
  const nodeTest = require('node:test')
8
9
 
@@ -28,6 +29,10 @@ const setSnapshotResolver = (fn) => {
28
29
  snapshot?.setResolveSnapshotPath(resolveSnapshot)
29
30
  }
30
31
 
32
+ const mockModule = mock.module
33
+ ? (t, o) => mock.module(t.includes('\\') ? pathToFileURL(t) : t, o) // resolve windows-looking paths
34
+ : undefined
35
+
31
36
  /* eslint-disable unicorn/no-useless-spread */
32
37
  module.exports = {
33
38
  engine: 'node:test',
@@ -35,7 +40,7 @@ module.exports = {
35
40
  ...{ mock, describe, test, beforeEach, afterEach, before, after },
36
41
  ...{ builtinModules, syncBuiltinESMExports },
37
42
  ...{ utilFormat, isPromise, nodeVersion, awaitForMicrotaskQueue },
38
- ...{ requireIsRelative, relativeRequire, isTopLevelESM },
43
+ ...{ requireIsRelative, relativeRequire, isTopLevelESM, mockModule },
39
44
  ...{ readSnapshot, setSnapshotSerializers, setSnapshotResolver },
40
45
  }
41
46
  /* eslint-enable unicorn/no-useless-spread */
@@ -17,6 +17,10 @@ const abstractProcess = globalThis.process || globalThis.EXODUS_TEST_PROCESS
17
17
 
18
18
  if (process.env.EXODUS_TEST_IS_BROWSER) {
19
19
  globalThis.EXODUS_TEST_PROMISE = new Promise((resolve) => (abstractProcess._exitHook = resolve))
20
+ if (!abstractProcess._maybeProcessExitCode && abstractProcess === globalThis.process) {
21
+ // Electron with Node.js integration has real process
22
+ process._maybeProcessExitCode = () => process._exitHook(process.exitCode ?? 0)
23
+ }
20
24
  }
21
25
 
22
26
  // assert module is slower
@@ -350,11 +354,11 @@ class MockTimers {
350
354
  this.#base = milliseconds
351
355
  }
352
356
 
353
- #setTimeout(callback, delay, ...args) {
357
+ #setTimeout(callback, delay = 0, ...args) {
354
358
  return this.#schedule({ callback, runAt: delay + this.#elapsed, args })
355
359
  }
356
360
 
357
- #setInterval(callback, delay, ...args) {
361
+ #setInterval(callback, delay = 0, ...args) {
358
362
  return this.#schedule({ callback, runAt: delay + this.#elapsed, interval: delay, args })
359
363
  }
360
364
 
@@ -475,21 +479,49 @@ const after = (fn) => context.addHook('after', fn)
475
479
 
476
480
  const isPromise = (x) => Boolean(x && x.then && x.catch && x.finally)
477
481
  const nodeVersion = '9999.99.99'
482
+
483
+ function getMacrotick() {
484
+ const { scheduler, MessageChannel } = globalThis
485
+ if (scheduler?.yield) return () => scheduler.yield()
486
+ if (setImmediate) return () => new Promise((resolve) => setImmediate(resolve))
487
+ if (MessageChannel) {
488
+ return async () => {
489
+ const { port1, port2 } = new MessageChannel()
490
+ await new Promise((resolve) => {
491
+ // eslint-disable-next-line unicorn/prefer-add-event-listener
492
+ port1.onmessage = resolve // also starts
493
+ port2.postMessage(0)
494
+ })
495
+ port2.close()
496
+ }
497
+ }
498
+
499
+ return null // no fallback
500
+ }
501
+
502
+ const macrotick = getMacrotick()
503
+
478
504
  const awaitForMicrotaskQueue = async () => {
505
+ // Scheduling an event at the end of current microtasks queue
479
506
  if (globalThis?.process?.nextTick) {
480
507
  if (globalThis.Bun) await Promise.resolve() // No idea what's up with Bun microtasks
481
- // We are in microtasks, awaiting for "next" tick will get us out of here
508
+ // We are in microtasks, scheduling a low-priority one will allow everything else to pass
509
+ // Except recursive process.nextTick calls, but that's acceptable
482
510
  return new Promise((resolve) => globalThis.process.nextTick(resolve))
483
511
  }
484
512
 
485
513
  // If that is not available, we can wait for the actual next cycle
486
- // For Hermes, we use -Xmicrotask-queue for this to act not like just a Promise.resolve().then(
514
+ // For Hermes, we use -Xmicrotask-queue for setImmediate to act not like just a Promise.resolve().then(
487
515
  // TODO: recheck if setImmediate is not faked with setTimeout if we enable a polyfill for it for JSC?
488
- if (setImmediate) return new Promise((resolve) => setImmediate(resolve))
516
+ // Browsers have scheduler.yield and/or MessageChannel which also perform macroticks
517
+ if (macrotick) return macrotick()
489
518
 
519
+ // If the above is not available, just create a chain of (high-priority) microtasks,
520
+ // hoping that'll allow other high-priority ones to pass
521
+ // Barebones like JSC and SpiderMonkey hit this currently
522
+ //
490
523
  // Do not rely on setTimeout here! it will tick actual time and is terribly slow (i.e. timers no longer fake)
491
524
  // 50_000 should be enough to flush everything that's going on in the microtask queue
492
- // E.g. JSC and SpiderMonkey hit this currently
493
525
  // engine262 is extremely slow, tick just above 100 on it
494
526
  const promise = Promise.resolve()
495
527
  const tickPromiseRounds = process.env.EXODUS_TEST_PLATFORM === 'engine262' ? 110 : 50_000
@@ -554,7 +586,7 @@ module.exports = {
554
586
  ...{ mock, describe, test, beforeEach, afterEach, before, after },
555
587
  ...{ builtinModules, syncBuiltinESMExports },
556
588
  ...{ utilFormat, isPromise, nodeVersion, awaitForMicrotaskQueue },
557
- ...{ requireIsRelative, relativeRequire, isTopLevelESM },
589
+ ...{ requireIsRelative, relativeRequire, isTopLevelESM, mockModule: mock.module },
558
590
  ...{ readSnapshot, setSnapshotSerializers, setSnapshotResolver },
559
591
  }
560
592
  /* eslint-enable unicorn/no-useless-spread */
@@ -1,5 +1,5 @@
1
1
  const nameCounts = new Map()
2
- let snapshotText
2
+ let snapshotText, snapshotTextClean
3
3
 
4
4
  const escapeSnapshot = (str) => str.replaceAll(/([\\`])/gu, '\\$1')
5
5
 
@@ -19,16 +19,18 @@ function matchSnapshot(readSnapshot, assert, name, serialized) {
19
19
  // We don't support polyfilled snapshot generation here, only parsing
20
20
  // Also be careful with assertion plan counters
21
21
  if (!snapshotText) assert.fail(`Could not find snapshot file. ${addFail}`)
22
+ if (!snapshotTextClean) snapshotTextClean = snapshotText.replaceAll('\r\n', '\n') // clean crlf
22
23
 
23
24
  const count = (nameCounts.get(name) || 0) + 1
24
25
  nameCounts.set(name, count)
25
26
  const escaped = escapeSnapshot(serialized)
26
27
  const key = `${name} ${count}`
27
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
28
30
  const final = escaped.includes('\n') ? `\n${escaped}\n` : escaped
29
- if (snapshotText.includes(makeEntry(final))) return
31
+ if (fixedText.includes(makeEntry(final))) return
30
32
  // Perhaps wrapped with newlines from Node.js snapshots?
31
- if (!final.includes('\n') && snapshotText.includes(makeEntry(`\n${final}\n`))) return
33
+ if (!final.includes('\n') && fixedText.includes(makeEntry(`\n${final}\n`))) return
32
34
  return assert.fail(`Could not match "${key}" in snapshot. ${addFail}`)
33
35
  }
34
36