@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.
- package/bin/browsers.js +9 -0
- package/bin/electron.js +5 -2
- package/bin/inband.js +2 -1
- package/bin/index.js +72 -35
- package/bin/reporter.js +3 -2
- package/loaders/babel.cjs +0 -2
- package/package.json +25 -63
- package/src/engine.js +2 -2
- package/src/engine.node.cjs +6 -1
- package/src/engine.pure.cjs +39 -7
- package/src/engine.pure.snapshot.cjs +5 -3
- package/src/jest.config.fs.js +2 -1
- package/src/jest.config.js +66 -6
- package/src/jest.js +3 -3
- package/src/jest.mock.js +10 -9
- package/src/version.js +1 -0
- package/bundler/babel-worker.cjs +0 -62
- package/bundler/bundle.js +0 -548
- package/bundler/modules/ansi-styles.cjs +0 -49
- package/bundler/modules/assert-strict.cjs +0 -1
- package/bundler/modules/child_process.cjs +0 -10
- package/bundler/modules/cluster.cjs +0 -27
- package/bundler/modules/crypto.cjs +0 -5
- package/bundler/modules/empty/function-throw.cjs +0 -4
- package/bundler/modules/empty/module-throw.cjs +0 -1
- package/bundler/modules/fs-promises.cjs +0 -1
- package/bundler/modules/fs.cjs +0 -123
- package/bundler/modules/globals.cjs +0 -340
- package/bundler/modules/globals.node.cjs +0 -8
- package/bundler/modules/http.cjs +0 -119
- package/bundler/modules/https.cjs +0 -11
- package/bundler/modules/jest-message-util.js +0 -5
- package/bundler/modules/jest-util.js +0 -22
- package/bundler/modules/module.cjs +0 -16
- package/bundler/modules/node-buffer.cjs +0 -3
- package/bundler/modules/text-encoding-utf.cjs +0 -90
- package/bundler/modules/tty.cjs +0 -10
- package/bundler/modules/url.cjs +0 -32
- package/bundler/modules/util-format.cjs +0 -48
- package/bundler/modules/util.cjs +0 -4
- 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
|
|
34
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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.
|
|
104
|
-
assert.
|
|
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
|
|
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 =
|
|
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(
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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 = [
|
|
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
|
-
|
|
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 <
|
|
641
|
-
// xs sometimes randomly crashes with SIGSEGV on CI. Allow
|
|
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,
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/test",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.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/
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
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
|
-
"
|
|
153
|
+
"jsdom": "^26.1.0",
|
|
154
|
+
"jsvu": "^3.0.0",
|
|
155
|
+
"prettier": "^3.0.3"
|
|
202
156
|
},
|
|
203
157
|
"peerDependencies": {
|
|
204
|
-
"
|
|
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@
|
|
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 }
|
package/src/engine.node.cjs
CHANGED
|
@@ -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 */
|
package/src/engine.pure.cjs
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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 (
|
|
31
|
+
if (fixedText.includes(makeEntry(final))) return
|
|
30
32
|
// Perhaps wrapped with newlines from Node.js snapshots?
|
|
31
|
-
if (!final.includes('\n') &&
|
|
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
|
|