@exodus/test 1.0.0-rc.26 → 1.0.0-rc.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +240 -17
- package/bin/jest.js +1 -2
- package/package.json +31 -4
- package/src/bundle-apis/ansi-styles.cjs +49 -0
- package/src/bundle-apis/assert-strict.cjs +1 -0
- package/src/bundle-apis/globals.cjs +58 -0
- package/src/bundle-apis/jest-message-util.js +5 -0
- package/src/bundle-apis/jest-util.js +22 -0
- package/src/dark.cjs +2 -2
- package/src/engine.js +2 -2
- package/src/engine.node.cjs +2 -1
- package/src/engine.pure.cjs +62 -22
- package/src/jest.config.js +25 -9
- package/src/jest.environment.js +58 -50
- package/src/jest.js +27 -21
- package/src/jest.mock.js +8 -3
package/bin/index.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process'
|
|
4
4
|
import { once } from 'node:events'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import { basename, dirname, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
+
import { basename, dirname, resolve, join } from 'node:path'
|
|
7
7
|
import { createRequire } from 'node:module'
|
|
8
|
+
import { randomUUID } from 'node:crypto'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
8
10
|
import assert from 'node:assert/strict'
|
|
9
11
|
import glob from 'fast-glob'
|
|
10
12
|
import { haveModuleMocks, haveSnapshots, haveForceExit } from '../src/version.js'
|
|
@@ -14,11 +16,17 @@ const bindir = dirname(fileURLToPath(import.meta.url))
|
|
|
14
16
|
const EXTS = `.?([cm])[jt]s?(x)` // we differ from jest, allowing [cm] before everything
|
|
15
17
|
const DEFAULT_PATTERNS = [`**/__tests__/**/*${EXTS}`, `**/?(*.)+(spec|test)${EXTS}`]
|
|
16
18
|
|
|
19
|
+
const bundleOptions = { pure: true, bundle: true, esbuild: true }
|
|
17
20
|
const ENGINES = new Map(
|
|
18
21
|
Object.entries({
|
|
19
22
|
'node:test': { binary: 'node', pure: false, hasImportLoader: true },
|
|
20
23
|
'node:pure': { binary: 'node', pure: true, hasImportLoader: true },
|
|
24
|
+
'node:bundle': { binary: 'node', ...bundleOptions },
|
|
21
25
|
'bun:pure': { binary: 'bun', pure: true, hasImportLoader: false },
|
|
26
|
+
'bun:bundle': { binary: 'bun', ...bundleOptions },
|
|
27
|
+
'deno:bundle': { binary: 'deno', binaryArgs: ['run'], target: 'deno1', ...bundleOptions },
|
|
28
|
+
'jsc:bundle': { binary: 'jsc', ...bundleOptions, target: 'safari11' },
|
|
29
|
+
'hermes:bundle': { binary: 'hermes', binaryArgs: ['-Og'], target: 'es2018', ...bundleOptions },
|
|
22
30
|
})
|
|
23
31
|
)
|
|
24
32
|
|
|
@@ -37,10 +45,6 @@ function parseOptions() {
|
|
|
37
45
|
debug: { files: false },
|
|
38
46
|
ideaCompat: false,
|
|
39
47
|
engine: process.env.EXODUS_TEST_ENGINE ?? 'node:test',
|
|
40
|
-
// Engine options
|
|
41
|
-
binary: 'node',
|
|
42
|
-
pure: false,
|
|
43
|
-
hasImportLoader: false,
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
const args = [...process.argv]
|
|
@@ -152,6 +156,11 @@ const resolveImport = import.meta.resolve && ((query) => fileURLToPath(import.me
|
|
|
152
156
|
|
|
153
157
|
const args = []
|
|
154
158
|
if (options.pure) {
|
|
159
|
+
if (options.bundle) {
|
|
160
|
+
assert(!options.coverage, `Can not use --coverage with ${options.engine} engine`)
|
|
161
|
+
assert(!options.babel, `Can not use --babel with ${options.engine} engine`) // TODO?
|
|
162
|
+
}
|
|
163
|
+
|
|
155
164
|
const requiresNodeCoverage = options.coverage && options.coverageEngine === 'node'
|
|
156
165
|
assert(!requiresNodeCoverage, '"--coverage-engine node" requires "--engine node:test" (default)')
|
|
157
166
|
assert(!options.writeSnapshots, `Can not use write snapshots with ${options.engine} engine`)
|
|
@@ -192,10 +201,14 @@ if (process.env.EXODUS_TEST_IGNORE) {
|
|
|
192
201
|
|
|
193
202
|
// The comment below is disabled, we don't auto-mock @jest/globals anymore, and having our loader first is faster
|
|
194
203
|
// [Disabled] Our loader should be last, as enabling module mocks confuses other loaders
|
|
204
|
+
let jestConfig = null
|
|
195
205
|
if (options.jest) {
|
|
196
206
|
const { loadJestConfig } = await import('../src/jest.config.js')
|
|
197
207
|
const config = await loadJestConfig(process.cwd())
|
|
198
|
-
|
|
208
|
+
jestConfig = config
|
|
209
|
+
if (!options.bundle) {
|
|
210
|
+
args.push(options.hasImportLoader ? '--import' : '-r', resolve(bindir, 'jest.js'))
|
|
211
|
+
}
|
|
199
212
|
|
|
200
213
|
if (config.testFailureExitCode !== undefined) {
|
|
201
214
|
if (Number(config.testFailureExitCode) === 0) {
|
|
@@ -229,12 +242,15 @@ if (options.jest) {
|
|
|
229
242
|
}
|
|
230
243
|
}
|
|
231
244
|
|
|
232
|
-
if (options.esbuild) {
|
|
245
|
+
if (options.esbuild && !options.bundle) {
|
|
233
246
|
assert(resolveImport)
|
|
234
247
|
if (options.hasImportLoader) {
|
|
235
248
|
args.push('--import', resolveImport('tsx'))
|
|
249
|
+
} else if (options.engine === process.env.EXODUS_TEST_ENGINE) {
|
|
250
|
+
console.warn(`Warning: ${options.engine} engine does not support --esbuild option`)
|
|
236
251
|
} else {
|
|
237
|
-
|
|
252
|
+
console.error(`Error: ${options.engine} engine does not support --esbuild option`)
|
|
253
|
+
process.exit(1)
|
|
238
254
|
}
|
|
239
255
|
}
|
|
240
256
|
|
|
@@ -326,18 +342,223 @@ if (options.coverage) {
|
|
|
326
342
|
}
|
|
327
343
|
}
|
|
328
344
|
|
|
329
|
-
assert(files.length > 0) // otherwise we can run recursively
|
|
330
|
-
assert(options.binary && ['node', 'bun', c8].includes(options.binary))
|
|
331
345
|
process.env.EXODUS_TEST_EXECARGV = JSON.stringify(args)
|
|
346
|
+
const inputs = files.map((file) => ({ source: file, file }))
|
|
347
|
+
|
|
348
|
+
if (options.bundle) {
|
|
349
|
+
const esbuild = await import('esbuild')
|
|
350
|
+
const { readFile, writeFile } = await import('node:fs/promises')
|
|
351
|
+
const { rmSync } = await import('node:fs')
|
|
352
|
+
const os = await import('node:os')
|
|
353
|
+
const outdir = join(os.tmpdir(), `exodus-test-${randomUUID().slice(0, 8)}`)
|
|
354
|
+
process.on('beforeExit', async () => rmSync(outdir, { recursive: true, force: true }))
|
|
355
|
+
assert.deepEqual(args, [])
|
|
356
|
+
if (options.binary === 'node') args.unshift('--enable-source-maps') // FIXME
|
|
357
|
+
|
|
358
|
+
const readSnapshots = async (ifiles) => {
|
|
359
|
+
const snapshots = []
|
|
360
|
+
for (const file of ifiles) {
|
|
361
|
+
for (const resolver of [
|
|
362
|
+
(dir, name) => [dir, `${name}.snapshot`], // node:test
|
|
363
|
+
(dir, name) => [dir, '__snapshots__', `${name}.snap`], // jest
|
|
364
|
+
]) {
|
|
365
|
+
const snapshotFile = join(...resolver(dirname(file), basename(file)))
|
|
366
|
+
try {
|
|
367
|
+
snapshots.push([snapshotFile, await readFile(snapshotFile, 'utf8')])
|
|
368
|
+
} catch (e) {
|
|
369
|
+
if (e.code !== 'ENOENT') throw e
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return snapshots
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const loadPipeline = [
|
|
378
|
+
function (source, args) {
|
|
379
|
+
return source
|
|
380
|
+
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(pathToFileURL(args.path)))
|
|
381
|
+
.replace(/\bimport\.meta\.dirname\b/g, JSON.stringify(dirname(args.path)))
|
|
382
|
+
.replace(/\bimport\.meta\.filename\b/g, JSON.stringify(basename(args.path)))
|
|
383
|
+
},
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
const writePipeline = []
|
|
387
|
+
if (options.binary === 'hermes') {
|
|
388
|
+
const babel = await import('@babel/core')
|
|
389
|
+
writePipeline.push((source) => {
|
|
390
|
+
const result = babel.transformSync(source, {
|
|
391
|
+
compact: false,
|
|
392
|
+
plugins: [
|
|
393
|
+
'@babel/plugin-transform-arrow-functions',
|
|
394
|
+
'@babel/plugin-transform-class-properties',
|
|
395
|
+
'@babel/plugin-transform-classes',
|
|
396
|
+
'@babel/plugin-transform-block-scoping',
|
|
397
|
+
],
|
|
398
|
+
})
|
|
399
|
+
return result.code
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const buildOne = async (...ifiles) => {
|
|
404
|
+
const input = []
|
|
405
|
+
const importSource = async (file) => input.push(await readFile(resolveRequire(file), 'utf8'))
|
|
406
|
+
const importFile = (...args) => input.push(`await import(${JSON.stringify(resolve(...args))});`)
|
|
407
|
+
|
|
408
|
+
if (!['node', c8].includes(options.binary)) await importSource('../src/bundle-apis/globals.cjs')
|
|
409
|
+
|
|
410
|
+
if (options.jest) {
|
|
411
|
+
assert(jestConfig.rootDir)
|
|
412
|
+
const preload = [...(jestConfig.setupFiles || []), ...(jestConfig.setupFilesAfterEnv || [])]
|
|
413
|
+
if (jestConfig.testEnvironment && jestConfig.testEnvironment !== 'node') {
|
|
414
|
+
const { specialEnvironments } = await import('../src/jest.environment.js')
|
|
415
|
+
assert(Object.hasOwn(specialEnvironments, jestConfig.testEnvironment))
|
|
416
|
+
preload.push(...(specialEnvironments[jestConfig.testEnvironment].dependencies || []))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const local = createRequire(resolve(jestConfig.rootDir, 'package.json'))
|
|
420
|
+
const w = (f) => `[${JSON.stringify(f)}, () => require(${JSON.stringify(local.resolve(f))})]`
|
|
421
|
+
input.push(`globalThis.EXODUS_TEST_PRELOADED = [${preload.map((f) => w(f)).join(', ')}]`)
|
|
422
|
+
await importSource('./jest.js')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
for (const file of ifiles) importFile(file)
|
|
426
|
+
const filename =
|
|
427
|
+
ifiles.length === 1 ? `${ifiles[0]}-${randomUUID().slice(0, 8)}` : `bundle-${randomUUID()}`
|
|
428
|
+
const outfile = `${join(outdir, filename)}.js`
|
|
429
|
+
const EXODUS_TEST_SNAPSHOTS = await readSnapshots(ifiles)
|
|
430
|
+
const build = async (opts) => esbuild.build(opts).catch((err) => ({ errors: [err] }))
|
|
431
|
+
let main = input.join('\n')
|
|
432
|
+
if (['jsc', 'hermes'].includes(options.binary)) {
|
|
433
|
+
main = `try {\n${main}\n} catch (err) { print(err); throw err }` // TODO: fix reporting
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const stringify = (x) => ([undefined, null].includes(x) ? `${x}` : JSON.stringify(x))
|
|
437
|
+
const res = await build({
|
|
438
|
+
stdin: {
|
|
439
|
+
contents: `(async function () {\n${main}\n})()`,
|
|
440
|
+
resolveDir: bindir,
|
|
441
|
+
},
|
|
442
|
+
bundle: true,
|
|
443
|
+
outdir,
|
|
444
|
+
entryNames: filename,
|
|
445
|
+
platform: 'neutral',
|
|
446
|
+
mainFields: ['browser', 'module', 'main'],
|
|
447
|
+
define: {
|
|
448
|
+
'process.env.FORCE_COLOR': stringify('0'),
|
|
449
|
+
'process.env.NO_COLOR': stringify('1'),
|
|
450
|
+
'process.env.NODE_ENV': stringify(process.env.NODE_ENV),
|
|
451
|
+
'process.env.EXODUS_TEST_CONTEXT': stringify('pure'),
|
|
452
|
+
'process.env.EXODUS_TEST_ENVIRONMENT': stringify('bundle'),
|
|
453
|
+
'process.env.EXODUS_TEST_PLATFORM': stringify(process.env.EXODUS_TEST_PLATFORM),
|
|
454
|
+
'process.env.EXODUS_TEST_JEST_CONFIG': stringify(JSON.stringify(jestConfig)),
|
|
455
|
+
'process.env.EXODUS_TEST_EXECARGV': stringify(process.env.EXODUS_TEST_EXECARGV),
|
|
456
|
+
'process.env.NODE_DEBUG': stringify(),
|
|
457
|
+
'process.env.READABLE_STREAM': stringify(),
|
|
458
|
+
'process.env.CI': stringify(process.env.CI),
|
|
459
|
+
'process.env.CI_ENABLE_VERBOSE_LOGS': stringify(process.env.CI_ENABLE_VERBOSE_LOGS),
|
|
460
|
+
'process.browser': stringify(true),
|
|
461
|
+
'process.emitWarning': 'undefined',
|
|
462
|
+
'process.stderr': 'undefined',
|
|
463
|
+
'process.stdout': 'undefined',
|
|
464
|
+
EXODUS_TEST_FILES: stringify(ifiles.map((f) => [dirname(f), basename(f)])),
|
|
465
|
+
EXODUS_TEST_SNAPSHOTS: stringify(EXODUS_TEST_SNAPSHOTS),
|
|
466
|
+
},
|
|
467
|
+
alias: {
|
|
468
|
+
// Node browserify
|
|
469
|
+
'node:assert': resolveRequire('assert'),
|
|
470
|
+
'node:assert/strict': resolveRequire('../src/bundle-apis/assert-strict.cjs'),
|
|
471
|
+
assert: resolveRequire('assert'),
|
|
472
|
+
buffer: resolveRequire('buffer'),
|
|
473
|
+
crypto: resolveRequire('crypto-browserify'),
|
|
474
|
+
events: resolveRequire('events'),
|
|
475
|
+
os: resolveRequire('os-browserify'),
|
|
476
|
+
path: resolveRequire('path-browserify'),
|
|
477
|
+
stream: resolveRequire('stream-browserify'),
|
|
478
|
+
util: resolveRequire('util'),
|
|
479
|
+
// expect-related deps
|
|
480
|
+
'ansi-styles': resolveRequire('../src/bundle-apis/ansi-styles.cjs'),
|
|
481
|
+
'jest-util': resolveRequire('../src/bundle-apis/jest-util.js'),
|
|
482
|
+
'jest-message-util': resolveRequire('../src/bundle-apis/jest-message-util.js'),
|
|
483
|
+
},
|
|
484
|
+
sourcemap: writePipeline.length > 0 ? 'inline' : 'linked',
|
|
485
|
+
sourcesContent: false,
|
|
486
|
+
keepNames: true,
|
|
487
|
+
format: 'iife',
|
|
488
|
+
target: options.target || `node${process.versions.node}`,
|
|
489
|
+
supported: {
|
|
490
|
+
bigint: true,
|
|
491
|
+
},
|
|
492
|
+
plugins: [
|
|
493
|
+
{
|
|
494
|
+
name: 'exodus-test.bundle',
|
|
495
|
+
setup({ onLoad }) {
|
|
496
|
+
onLoad({ filter: /\.m?js$/, namespace: 'file' }, async (args) => {
|
|
497
|
+
let contents = await readFile(args.path, 'utf8')
|
|
498
|
+
for (const transform of loadPipeline) contents = await transform(contents, args)
|
|
499
|
+
return { contents }
|
|
500
|
+
})
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
if (writePipeline.length > 0 && res.errors.length === 0) {
|
|
507
|
+
let contents = await readFile(outfile, 'utf8')
|
|
508
|
+
for (const transform of writePipeline) contents = await transform(contents)
|
|
509
|
+
await writeFile(outfile, contents)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// require('fs').copyFileSync(outfile, 'tempout.cjs') // DEBUG
|
|
513
|
+
return { file: outfile, errors: res.errors, warnings: res.warnings }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
for (const input of inputs) Object.assign(input, await buildOne(input.file)) // TODO: queued concurrency
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
assert.equal(inputs.length, files.length)
|
|
520
|
+
assert(options.binary && ['node', 'bun', 'deno', 'jsc', 'hermes', c8].includes(options.binary))
|
|
332
521
|
|
|
333
522
|
if (options.pure) {
|
|
523
|
+
if (options.binary === 'hermes') {
|
|
524
|
+
const dir = dirname(require.resolve('hermes-engine-cli/package.json'))
|
|
525
|
+
switch (process.platform) {
|
|
526
|
+
case 'darwin':
|
|
527
|
+
process.env.PATH = `${join(dir, 'osx-bin')}:${process.env.PATH}`
|
|
528
|
+
break
|
|
529
|
+
case 'linux':
|
|
530
|
+
process.env.PATH = `${join(dir, 'linux64-bin')}:${process.env.PATH}`
|
|
531
|
+
break
|
|
532
|
+
case 'win32':
|
|
533
|
+
process.env.PATH = `${join(dir, 'win64-bin')}:${process.env.PATH}`
|
|
534
|
+
break
|
|
535
|
+
default:
|
|
536
|
+
assert.fail(`Unexpected platform: ${process.platform}`)
|
|
537
|
+
}
|
|
538
|
+
} else if (options.binary === 'jsc' && process.platform === 'darwin') {
|
|
539
|
+
const prefix = '/System/Library/Frameworks/JavaScriptCore.framework/Versions/A'
|
|
540
|
+
for (const dir of [`${prefix}/Helpers`, `${prefix}/Resources`]) {
|
|
541
|
+
if (existsSync(join(dir, 'jsc'))) {
|
|
542
|
+
process.env.PATH = `${dir}:${process.env.PATH}`
|
|
543
|
+
break
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
334
548
|
process.env.EXODUS_TEST_CONTEXT = 'pure'
|
|
335
549
|
console.warn(`\n${options.engine} engine is experimental and may not work an expected\n\n`)
|
|
336
550
|
const failures = []
|
|
337
|
-
for (const
|
|
338
|
-
|
|
551
|
+
for (const input of inputs) {
|
|
552
|
+
if (input.errors?.length > 0 || input.warnings?.length > 0) {
|
|
553
|
+
failures.push(input.source)
|
|
554
|
+
continue
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const { binaryArgs = [] } = options
|
|
558
|
+
console.log(`# ${input.source}`)
|
|
559
|
+
const node = spawn(options.binary, [...binaryArgs, ...args, input.file], { stdio: 'inherit' })
|
|
339
560
|
const [code] = await once(node, 'close')
|
|
340
|
-
if (code !== 0) failures.push(
|
|
561
|
+
if (code !== 0) failures.push(input.source)
|
|
341
562
|
}
|
|
342
563
|
|
|
343
564
|
if (failures.length > 0) {
|
|
@@ -350,9 +571,11 @@ if (options.pure) {
|
|
|
350
571
|
console.log(`All ${files.length} test suites passed`)
|
|
351
572
|
}
|
|
352
573
|
} else {
|
|
353
|
-
assert(['node', c8].includes(options.binary),
|
|
354
|
-
assert
|
|
355
|
-
process.env.EXODUS_TEST_CONTEXT =
|
|
574
|
+
assert(['node', c8].includes(options.binary), `Unexpected native engine: ${options.binary}`)
|
|
575
|
+
assert(['node:test'].includes(options.engine))
|
|
576
|
+
process.env.EXODUS_TEST_CONTEXT = options.engine
|
|
577
|
+
assert(files.length > 0) // otherwise we can run recursively
|
|
578
|
+
assert(!options.binaryArgs)
|
|
356
579
|
const node = spawn(options.binary, [...args, ...files], { stdio: 'inherit' })
|
|
357
580
|
const [code] = await once(node, 'close')
|
|
358
581
|
process.exitCode = code
|
package/bin/jest.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const { loadJestConfig, installJestEnvironment } = await import('../src/jest.config.js')
|
|
3
2
|
await loadJestConfig()
|
|
4
3
|
const jestGlobals = await import('../src/jest.js')
|
|
5
4
|
await installJestEnvironment(jestGlobals)
|
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.28",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"bin/babel.cjs",
|
|
39
39
|
"bin/jest.js",
|
|
40
|
+
"src/bundle-apis/ansi-styles.cjs",
|
|
41
|
+
"src/bundle-apis/assert-strict.cjs",
|
|
42
|
+
"src/bundle-apis/globals.cjs",
|
|
43
|
+
"src/bundle-apis/jest-message-util.js",
|
|
44
|
+
"src/bundle-apis/jest-util.js",
|
|
45
|
+
"src/bundle-apis/util-format.js",
|
|
40
46
|
"src/dark.cjs",
|
|
41
47
|
"src/engine.js",
|
|
42
48
|
"src/engine.node.cjs",
|
|
@@ -58,8 +64,8 @@
|
|
|
58
64
|
],
|
|
59
65
|
"scripts": {
|
|
60
66
|
"test": "npm run test:jest",
|
|
61
|
-
"test:all": "npm run test:jest && npm run test:tape && npm run test:native",
|
|
62
|
-
"test:native": "EXODUS_TEST_IGNORE='{**/typescript/**,**/jest-repo/**/user.test.js}' ./bin/index.js --jest '__test__/**/*.test.js'",
|
|
67
|
+
"test:all": "npm run test:jest && npm run test:tape && npm run test:native && npm run test:pure",
|
|
68
|
+
"test:native": "EXODUS_TEST_IGNORE='{**/typescript/**,**/jest-repo/**/user.test.js}' ./bin/index.js --jest '__test__/**/*.test.{js,cjs,mjs}'",
|
|
63
69
|
"test:jest": "./bin/index.js --jest --esbuild",
|
|
64
70
|
"test:tape": "./bin/index.js --esbuild '__test__/tape/test/*.js' __test__/tape.test.js",
|
|
65
71
|
"test:pure": "EXODUS_TEST_ENGINE=node:pure npm run test",
|
|
@@ -69,15 +75,28 @@
|
|
|
69
75
|
"lint:fix": "prettier --write . && eslint --fix ."
|
|
70
76
|
},
|
|
71
77
|
"dependencies": {
|
|
78
|
+
"@babel/core": "^7.24.9",
|
|
79
|
+
"@babel/plugin-transform-arrow-functions": "^7.24.7",
|
|
80
|
+
"@babel/plugin-transform-block-scoping": "^7.24.7",
|
|
81
|
+
"@babel/plugin-transform-class-properties": "^7.24.7",
|
|
82
|
+
"@babel/plugin-transform-classes": "^7.24.8",
|
|
72
83
|
"@babel/register": "^7.0.0",
|
|
84
|
+
"assert": "^2.1.0",
|
|
85
|
+
"buffer": "^6.0.3",
|
|
73
86
|
"c8": "^9.1.0",
|
|
87
|
+
"crypto-browserify": "^3.12.0",
|
|
74
88
|
"esbuild": "~0.21.5",
|
|
89
|
+
"events": "^3.3.0",
|
|
75
90
|
"expect": "^29.7.0",
|
|
76
91
|
"fast-glob": "^3.2.11",
|
|
77
92
|
"jest-extended": "^4.0.2",
|
|
78
93
|
"jsdom": "^24.1.0",
|
|
94
|
+
"os-browserify": "^0.3.0",
|
|
95
|
+
"path-browserify": "^1.0.1",
|
|
79
96
|
"pretty-format": "^29.7.0",
|
|
80
|
-
"
|
|
97
|
+
"stream-browserify": "^3.0.0",
|
|
98
|
+
"tsx": "^4.16.2",
|
|
99
|
+
"util": "^0.12.5"
|
|
81
100
|
},
|
|
82
101
|
"devDependencies": {
|
|
83
102
|
"@exodus/eslint-config": "^5.24.0",
|
|
@@ -91,5 +110,13 @@
|
|
|
91
110
|
"jest-serializer-ansi-escapes": "^3.0.0",
|
|
92
111
|
"jest-when": "^3.6.0"
|
|
93
112
|
},
|
|
113
|
+
"peerDependencies": {
|
|
114
|
+
"hermes-engine-cli": "^0.12.0"
|
|
115
|
+
},
|
|
116
|
+
"peerDependenciesMeta": {
|
|
117
|
+
"hermes-engine-cli": {
|
|
118
|
+
"optional": true
|
|
119
|
+
}
|
|
120
|
+
},
|
|
94
121
|
"packageManager": "pnpm@9.4.0+sha1.9217c800d4ab947a7aee520242a7b70d64fc7638"
|
|
95
122
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const colors = [
|
|
2
|
+
'reset',
|
|
3
|
+
'bold',
|
|
4
|
+
'dim',
|
|
5
|
+
'italic',
|
|
6
|
+
'underline',
|
|
7
|
+
'overline',
|
|
8
|
+
'inverse',
|
|
9
|
+
'hidden',
|
|
10
|
+
'strikethrough',
|
|
11
|
+
'black',
|
|
12
|
+
'red',
|
|
13
|
+
'green',
|
|
14
|
+
'yellow',
|
|
15
|
+
'blue',
|
|
16
|
+
'magenta',
|
|
17
|
+
'cyan',
|
|
18
|
+
'white',
|
|
19
|
+
'blackBright',
|
|
20
|
+
'redBright',
|
|
21
|
+
'greenBright',
|
|
22
|
+
'yellowBright',
|
|
23
|
+
'blueBright',
|
|
24
|
+
'magentaBright',
|
|
25
|
+
'cyanBright',
|
|
26
|
+
'whiteBright',
|
|
27
|
+
'gray',
|
|
28
|
+
'grey',
|
|
29
|
+
'bgBlack',
|
|
30
|
+
'bgRed',
|
|
31
|
+
'bgGreen',
|
|
32
|
+
'bgYellow',
|
|
33
|
+
'bgBlue',
|
|
34
|
+
'bgMagenta',
|
|
35
|
+
'bgCyan',
|
|
36
|
+
'bgWhite',
|
|
37
|
+
'bgBlackBright',
|
|
38
|
+
'bgRedBright',
|
|
39
|
+
'bgGreenBright',
|
|
40
|
+
'bgYellowBright',
|
|
41
|
+
'bgBlueBright',
|
|
42
|
+
'bgMagentaBright',
|
|
43
|
+
'bgCyanBright',
|
|
44
|
+
'bgWhiteBright',
|
|
45
|
+
'bgGray',
|
|
46
|
+
'bgGrey',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
for (const key of colors) exports[key] = { open: '', close: '' }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('node:assert').strict
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
if (!globalThis.global) globalThis.global = globalThis
|
|
2
|
+
if (!globalThis.Buffer) globalThis.Buffer = require('buffer').Buffer
|
|
3
|
+
if (!globalThis.console) {
|
|
4
|
+
// eslint-disable-next-line no-undef
|
|
5
|
+
globalThis.console = { log: print, error: print, warn: print, info: print, debug: print }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!globalThis.fetch) {
|
|
9
|
+
globalThis.fetch = () => {
|
|
10
|
+
throw new Error('Fetch not supported')
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!globalThis.WebSocket) {
|
|
15
|
+
globalThis.WebSocket = () => {
|
|
16
|
+
throw new Error('WebSocket not supported')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!Array.prototype.at) {
|
|
21
|
+
// eslint-disable-next-line no-extend-native
|
|
22
|
+
Array.prototype.at = function (i) {
|
|
23
|
+
return this[i < 0 ? this.length + i : i]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (globalThis.describe) delete globalThis.describe
|
|
28
|
+
|
|
29
|
+
if (process.env.EXODUS_TEST_PLATFORM === 'hermes') {
|
|
30
|
+
// Ok, we have broken timers, let's hack them around
|
|
31
|
+
let i = 0
|
|
32
|
+
const timers = new Map()
|
|
33
|
+
const { setTimeout, clearTimeout } = globalThis
|
|
34
|
+
const dateNow = Date.now
|
|
35
|
+
globalThis.setTimeout = (fn, time) => {
|
|
36
|
+
const id = `ht${i++}`
|
|
37
|
+
const now = dateNow()
|
|
38
|
+
const tick = () => {
|
|
39
|
+
if (!timers.has(id)) return
|
|
40
|
+
const remaining = now + time - dateNow()
|
|
41
|
+
if (remaining < 0) {
|
|
42
|
+
timers.delete(id)
|
|
43
|
+
fn()
|
|
44
|
+
} else {
|
|
45
|
+
timers.set(id, setTimeout(tick, remaining))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
timers.set(id, setTimeout(tick, time))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
globalThis.clearTimeout = (id) => {
|
|
53
|
+
if (!timers.has(id)) return
|
|
54
|
+
clearTimeout(timers.get(id))
|
|
55
|
+
timers.delete(id)
|
|
56
|
+
}
|
|
57
|
+
// TODO: setInterval, clearInterval
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// APIs used in expect()
|
|
2
|
+
|
|
3
|
+
export { isPromise } from '../engine.js'
|
|
4
|
+
|
|
5
|
+
var NUMS = [
|
|
6
|
+
'zero',
|
|
7
|
+
'one',
|
|
8
|
+
'two',
|
|
9
|
+
'three',
|
|
10
|
+
'four',
|
|
11
|
+
'five',
|
|
12
|
+
'six',
|
|
13
|
+
'seven',
|
|
14
|
+
'eight',
|
|
15
|
+
'nine',
|
|
16
|
+
'ten',
|
|
17
|
+
'eleven',
|
|
18
|
+
'twelve',
|
|
19
|
+
'thirteen',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export const pluralize = (word, count) => `${NUMS[count] || count} ${word}${count === 1 ? '' : 's'}`
|
package/src/dark.cjs
CHANGED
|
@@ -51,7 +51,7 @@ function createCallerLocationHook() {
|
|
|
51
51
|
return { installLocationInNextTest, getCallerLocation }
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Easy on Node.js >= 22.3.0, but we polyfill for the rest
|
|
54
|
+
// Easy on Node.js >= 22.3.0 || ^20.16.0, but we polyfill for the rest
|
|
55
55
|
function getTestNamePath(t) {
|
|
56
56
|
// No implementation in Node.js yet, will have to PR
|
|
57
57
|
if (t.fullName) return t.fullName.split(' > ')
|
|
@@ -108,7 +108,7 @@ function getTestNamePath(t) {
|
|
|
108
108
|
const execArgv = process.env.EXODUS_TEST_EXECARGV
|
|
109
109
|
? JSON.parse(process.env.EXODUS_TEST_EXECARGV)
|
|
110
110
|
: process.execArgv
|
|
111
|
-
const insideEsbuild = execArgv.some((x) => x.
|
|
111
|
+
const insideEsbuild = execArgv.some((x) => x.endsWith('node_modules/tsx/dist/loader.mjs'))
|
|
112
112
|
|
|
113
113
|
function makeEsbuildMockable() {
|
|
114
114
|
if (!insideEsbuild) return
|
package/src/engine.js
CHANGED
|
@@ -15,8 +15,8 @@ export { builtinModules, syncBuiltinESMExports }
|
|
|
15
15
|
const { utilFormat, isPromise, nodeVersion } = engine
|
|
16
16
|
export { utilFormat, isPromise, nodeVersion }
|
|
17
17
|
|
|
18
|
-
const {
|
|
19
|
-
export {
|
|
18
|
+
const { requireIsRelative, relativeRequire, isTopLevelESM } = engine
|
|
19
|
+
export { requireIsRelative, relativeRequire, isTopLevelESM }
|
|
20
20
|
|
|
21
21
|
const { readSnapshot, setSnapshotSerializers, setSnapshotResolver } = engine
|
|
22
22
|
export { readSnapshot, setSnapshotSerializers, setSnapshotResolver }
|
package/src/engine.node.cjs
CHANGED
|
@@ -14,6 +14,7 @@ const nodeVersion = process.versions.node
|
|
|
14
14
|
const files = process.argv.slice(1)
|
|
15
15
|
const baseFile = files.length === 1 && existsSync(files[0]) ? normalize(files[0]) : undefined
|
|
16
16
|
const relativeRequire = baseFile ? createRequire(baseFile) : require
|
|
17
|
+
const requireIsRelative = Boolean(baseFile)
|
|
17
18
|
const isTopLevelESM = () => !baseFile || !Object.hasOwn(relativeRequire.cache, baseFile) // assume ESM otherwise
|
|
18
19
|
|
|
19
20
|
const snapshot = nodeTest.snapshot
|
|
@@ -33,7 +34,7 @@ module.exports = {
|
|
|
33
34
|
...{ mock, describe, test, beforeEach, afterEach, before, after },
|
|
34
35
|
...{ builtinModules, syncBuiltinESMExports },
|
|
35
36
|
...{ utilFormat, isPromise, nodeVersion },
|
|
36
|
-
...{
|
|
37
|
+
...{ requireIsRelative, relativeRequire, isTopLevelESM },
|
|
37
38
|
...{ readSnapshot, setSnapshotSerializers, setSnapshotResolver },
|
|
38
39
|
}
|
|
39
40
|
/* eslint-enable unicorn/no-useless-spread */
|
package/src/engine.pure.cjs
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
const assert = require('node:assert/strict')
|
|
2
2
|
const assertLoose = require('node:assert')
|
|
3
|
-
const { existsSync, readFileSync } = require('node:fs')
|
|
4
|
-
const { normalize, basename, dirname, join: pathJoin } = require('node:path')
|
|
5
|
-
const { format: utilFormat } = require('node:util')
|
|
6
|
-
const { createRequire, builtinModules, syncBuiltinESMExports } = require('node:module')
|
|
7
3
|
|
|
8
4
|
const { setTimeout, setInterval, setImmediate, Date } = globalThis
|
|
9
5
|
const { clearTimeout, clearInterval, clearImmediate } = globalThis
|
|
@@ -57,6 +53,14 @@ async function runFunction(fn, context) {
|
|
|
57
53
|
return new Promise((resolve, reject) => fn(context, (err) => (err ? reject(err) : resolve())))
|
|
58
54
|
}
|
|
59
55
|
|
|
56
|
+
let fallbackExitCode = 0
|
|
57
|
+
const failExitCode = (...args) => {
|
|
58
|
+
console.log(...args)
|
|
59
|
+
if (globalThis.process) return (globalThis.process.exitCode = 1)
|
|
60
|
+
if (globalThis.Deno) return (globalThis.Deno.exitCode = 1)
|
|
61
|
+
return (fallbackExitCode = 1)
|
|
62
|
+
}
|
|
63
|
+
|
|
60
64
|
async function runContext(context) {
|
|
61
65
|
const { options, children, hooks, fn } = context
|
|
62
66
|
assert(!context.running, 'Can not run twice')
|
|
@@ -81,10 +85,7 @@ async function runContext(context) {
|
|
|
81
85
|
for (const c of stack) for (const hook of c.hooks.afterEach) await runFunction(hook, context)
|
|
82
86
|
|
|
83
87
|
console.log(error === undefined ? '✔ PASS' : '✖ FAIL', context.fullName)
|
|
84
|
-
if (error)
|
|
85
|
-
console.log(' ', error)
|
|
86
|
-
if (globalThis.process) globalThis.process.exitCode = 1
|
|
87
|
-
}
|
|
88
|
+
if (error) failExitCode(' ', error)
|
|
88
89
|
} else {
|
|
89
90
|
// if (context !== context.root) console.log(`▶ ${context.fullName}`)
|
|
90
91
|
// TODO: try/catch for hooks?
|
|
@@ -100,11 +101,15 @@ async function run() {
|
|
|
100
101
|
assert(!running)
|
|
101
102
|
running = true
|
|
102
103
|
assert(context === context.root)
|
|
103
|
-
runContext(context).catch((error) => {
|
|
104
|
+
await runContext(context).catch((error) => {
|
|
104
105
|
// Should not throw under regular circumstances
|
|
105
|
-
|
|
106
|
-
if (globalThis.process) globalThis.process.exitCode = 1
|
|
106
|
+
failExitCode('Fatal: ', error)
|
|
107
107
|
})
|
|
108
|
+
if (fallbackExitCode !== 0) {
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
throw new Error('Test failed')
|
|
111
|
+
}, 0)
|
|
112
|
+
}
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
function describe(...args) {
|
|
@@ -152,7 +157,19 @@ class MockTimers {
|
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
if (apis.includes('setImmediate')) {
|
|
155
|
-
|
|
160
|
+
if (process.env.EXODUS_TEST_PLATFORM === 'hermes') {
|
|
161
|
+
// Sigh, these are used internally
|
|
162
|
+
const isInternal = (x) =>
|
|
163
|
+
x.includes('at handleResolved ') || x.includes('/InternalBytecode/InternalBytecode')
|
|
164
|
+
globalThis.setImmediate = (...args) => {
|
|
165
|
+
const { stack } = new Error() // eslint-disable-line unicorn/error-message
|
|
166
|
+
if (isInternal(stack.split('\n')[2])) return setImmediate(...args)
|
|
167
|
+
return this.#setImmediate(...args)
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
globalThis.setImmediate = this.#setImmediate.bind(this)
|
|
171
|
+
}
|
|
172
|
+
|
|
156
173
|
globalThis.clearImmediate = this.#clearImmediate.bind(this)
|
|
157
174
|
}
|
|
158
175
|
|
|
@@ -306,17 +323,40 @@ const after = (fn) => context.hooks.after.push(fn)
|
|
|
306
323
|
const isPromise = (x) => Boolean(x && x.then && x.catch && x.finally)
|
|
307
324
|
const nodeVersion = '9999.99.99'
|
|
308
325
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
326
|
+
let builtinModules = []
|
|
327
|
+
let requireIsRelative = false
|
|
328
|
+
let relativeRequire, isTopLevelESM, syncBuiltinESMExports, readSnapshot, utilFormat
|
|
329
|
+
if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
330
|
+
// eslint-disable-next-line no-undef
|
|
331
|
+
const files = EXODUS_TEST_FILES
|
|
332
|
+
const baseFile = files.length === 1 ? files[0] : undefined
|
|
333
|
+
isTopLevelESM = () => false
|
|
334
|
+
// eslint-disable-next-line no-undef
|
|
335
|
+
const bundleSnaps = typeof EXODUS_TEST_SNAPSHOTS !== 'undefined' && new Map(EXODUS_TEST_SNAPSHOTS)
|
|
336
|
+
const resolveSnapshot = (f) => snapshotResolver(f[0], f[1]).join('/')
|
|
337
|
+
readSnapshot = (f = baseFile) => (f ? bundleSnaps.get(resolveSnapshot(f)) : null)
|
|
338
|
+
utilFormat = require('./bundle-apis/util-format.cjs')
|
|
339
|
+
} else {
|
|
340
|
+
const { existsSync, readFileSync } = require('node:fs')
|
|
341
|
+
const { dirname, basename, normalize, join } = require('node:path')
|
|
342
|
+
const nodeModule = require('node:module')
|
|
343
|
+
const files = process.argv.slice(1)
|
|
344
|
+
const baseFile = files.length === 1 && existsSync(files[0]) ? normalize(files[0]) : undefined
|
|
345
|
+
requireIsRelative = Boolean(baseFile)
|
|
346
|
+
relativeRequire = baseFile ? nodeModule.createRequire(baseFile) : require
|
|
347
|
+
isTopLevelESM = () =>
|
|
348
|
+
!baseFile || // assume ESM otherwise
|
|
349
|
+
!Object.hasOwn(relativeRequire.cache, baseFile) || // node esm
|
|
350
|
+
relativeRequire.cache[baseFile].exports[Symbol.toStringTag] === 'Module' // bun esm
|
|
351
|
+
const resolveSnapshot = (f) => join(...snapshotResolver(dirname(f), basename(f)))
|
|
352
|
+
readSnapshot = (f = baseFile) => (f ? readFileSync(resolveSnapshot(f), 'utf8') : null)
|
|
353
|
+
builtinModules = nodeModule.builtinModules
|
|
354
|
+
syncBuiltinESMExports = nodeModule.syncBuiltinESMExports || nodeModule.syncBuiltinExports // bun has it under a different name (also a no-op and always synced atm)
|
|
355
|
+
utilFormat = require('node:util').format
|
|
356
|
+
}
|
|
316
357
|
|
|
358
|
+
// eslint-disable-next-line no-undef
|
|
317
359
|
let snapshotResolver = (dir, name) => [dir, `${name}.snapshot`] // default per Node.js docs
|
|
318
|
-
const resolveSnapshot = (f) => pathJoin(...snapshotResolver(dirname(f), basename(f)))
|
|
319
|
-
const readSnapshot = (f = baseFile) => (f ? readFileSync(resolveSnapshot(f), 'utf8') : null)
|
|
320
360
|
const setSnapshotSerializers = () => {}
|
|
321
361
|
const setSnapshotResolver = (fn) => {
|
|
322
362
|
snapshotResolver = fn
|
|
@@ -331,7 +371,7 @@ module.exports = {
|
|
|
331
371
|
...{ mock, describe, test, beforeEach, afterEach, before, after },
|
|
332
372
|
...{ builtinModules, syncBuiltinESMExports },
|
|
333
373
|
...{ utilFormat, isPromise, nodeVersion },
|
|
334
|
-
...{
|
|
374
|
+
...{ requireIsRelative, relativeRequire, isTopLevelESM },
|
|
335
375
|
...{ readSnapshot, setSnapshotSerializers, setSnapshotResolver },
|
|
336
376
|
}
|
|
337
377
|
/* eslint-enable unicorn/no-useless-spread */
|
package/src/jest.config.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// Not using ./engine.js yet, might pass / embed already loaded config instead
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { createRequire } from 'node:module'
|
|
5
3
|
import { specialEnvironments } from './jest.environment.js'
|
|
6
4
|
|
|
7
5
|
const normalizeJestConfig = (config) => ({
|
|
@@ -75,6 +73,12 @@ export async function loadJestConfig(...args) {
|
|
|
75
73
|
|
|
76
74
|
config = normalizeJestConfig(rawConfig)
|
|
77
75
|
verifyJestConfig(config)
|
|
76
|
+
|
|
77
|
+
// require is already relative to rootDir
|
|
78
|
+
const cleanFile = (file) => file.replace(/^<rootDir>\//g, './')
|
|
79
|
+
config.setupFiles = config.setupFiles?.map((f) => cleanFile(f))
|
|
80
|
+
config.setupFilesAfterEnv = config.setupFilesAfterEnv?.map((f) => cleanFile(f))
|
|
81
|
+
|
|
78
82
|
return config
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -95,17 +99,29 @@ export async function installJestEnvironment(jestGlobals) {
|
|
|
95
99
|
if (c.restoreMocks) beforeEach(() => jest.restoreAllMocks())
|
|
96
100
|
if (c.resetModules) beforeEach(() => jest.resetModules())
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
let require
|
|
103
|
+
if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
104
|
+
const preloaded = new Map(EXODUS_TEST_PRELOADED) // eslint-disable-line no-undef
|
|
105
|
+
require = (name) => {
|
|
106
|
+
if (preloaded.has(name)) return preloaded.get(name)()
|
|
107
|
+
assert.fail('Requiring non-bundled plugins from bundle is unsupported')
|
|
108
|
+
}
|
|
109
|
+
} else if (config.rootDir) {
|
|
110
|
+
const { resolve } = await import('node:path')
|
|
111
|
+
const { createRequire } = await import('node:module')
|
|
112
|
+
require = createRequire(resolve(config.rootDir, 'package.json'))
|
|
113
|
+
} else {
|
|
114
|
+
require = () => assert.fail('Unreachable: requiring plugins without a rootDir')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const file of c.setupFiles || []) require(file)
|
|
101
118
|
|
|
102
119
|
if (Object.hasOwn(specialEnvironments, c.testEnvironment)) {
|
|
103
|
-
specialEnvironments[c.testEnvironment]
|
|
120
|
+
const { setup } = specialEnvironments[c.testEnvironment]
|
|
121
|
+
await setup(require, engine, jestGlobals, c.testEnvironmentOptions)
|
|
104
122
|
}
|
|
105
123
|
|
|
106
|
-
|
|
107
|
-
for (const file of c.setupFiles || []) require(file.replace(/^<rootDir>\//g, './'))
|
|
108
|
-
for (const file of c.setupFilesAfterEnv || []) require(file.replace(/^<rootDir>\//g, './'))
|
|
124
|
+
for (const file of c.setupFilesAfterEnv || []) require(file)
|
|
109
125
|
|
|
110
126
|
// @jest/globals import auto-mocking is disabled until https://github.com/nodejs/node/issues/53807 is resolved
|
|
111
127
|
/*
|
package/src/jest.environment.js
CHANGED
|
@@ -1,64 +1,72 @@
|
|
|
1
|
-
import { getTestNamePath } from './dark.cjs'
|
|
2
|
-
|
|
3
1
|
export const specialEnvironments = {
|
|
4
2
|
__proto__: null,
|
|
5
3
|
|
|
6
|
-
jsdom:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
4
|
+
jsdom: {
|
|
5
|
+
dependencies: ['jsdom'],
|
|
6
|
+
setup: (require) => {
|
|
7
|
+
const { JSDOM, VirtualConsole } = require('jsdom')
|
|
8
|
+
const virtualConsole = new VirtualConsole()
|
|
9
|
+
const { window } = new JSDOM('<!DOCTYPE html>', {
|
|
10
|
+
url: 'http://localhost/',
|
|
11
|
+
pretendToBeVisual: true,
|
|
12
|
+
runScripts: 'dangerously',
|
|
13
|
+
virtualConsole,
|
|
14
|
+
})
|
|
15
|
+
virtualConsole.sendTo(console, { omitJSDOMErrors: true })
|
|
16
|
+
virtualConsole.on('jsdomError', (error) => {
|
|
17
|
+
throw error
|
|
18
|
+
})
|
|
19
|
+
const assignMissing = (target, source) => {
|
|
20
|
+
const entries = Object.entries(source).filter(([key]) => !Object.hasOwn(target, key))
|
|
21
|
+
Object.assign(target, Object.fromEntries(entries))
|
|
22
|
+
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
assignMissing(globalThis, window)
|
|
25
|
+
assignMissing(console, window.console)
|
|
26
|
+
Object.setPrototypeOf(global, Object.getPrototypeOf(window))
|
|
27
|
+
},
|
|
27
28
|
},
|
|
28
29
|
|
|
29
30
|
// Reproduces setup-polly-jest/jest-environment-node ad hacks into 'setup-polly-jest'.pollyJest
|
|
30
|
-
'setup-polly-jest/jest-environment-node':
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
'setup-polly-jest/jest-environment-node': {
|
|
32
|
+
dependencies: ['@pollyjs/core', 'setup-polly-jest', 'setup-polly-jest/lib/common'],
|
|
33
|
+
setup: async (require, engine) => {
|
|
34
|
+
const { getTestNamePath } = await import('./dark.cjs')
|
|
35
|
+
const { Polly } = require('@pollyjs/core')
|
|
36
|
+
const pollyJest = require('setup-polly-jest')
|
|
37
|
+
const {
|
|
38
|
+
JestPollyGlobals,
|
|
39
|
+
createPollyContextAccessor,
|
|
40
|
+
} = require('setup-polly-jest/lib/common')
|
|
41
|
+
const pollyGlobals = new JestPollyGlobals(globalThis)
|
|
42
|
+
pollyGlobals.isJestPollyEnvironment = true
|
|
43
|
+
pollyJest.setupPolly = (options) => {
|
|
44
|
+
if (!pollyGlobals.isJestPollyEnvironment) return
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
engine.before(() => {
|
|
47
|
+
pollyGlobals.isPollyActive = true
|
|
48
|
+
pollyGlobals.pollyContext.options = options
|
|
49
|
+
})
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
engine.after(() => {
|
|
52
|
+
pollyGlobals.isPollyActive = false
|
|
53
|
+
pollyGlobals.pollyContext.options = null
|
|
54
|
+
})
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
return createPollyContextAccessor(pollyGlobals)
|
|
57
|
+
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
engine.beforeEach((t) => {
|
|
60
|
+
if (!pollyGlobals.isPollyActive) return
|
|
61
|
+
const name = getTestNamePath(t).join('/')
|
|
62
|
+
pollyGlobals.pollyContext.polly = new Polly(name, pollyGlobals.pollyContext.options)
|
|
63
|
+
})
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
engine.afterEach(async () => {
|
|
66
|
+
if (!pollyGlobals.pollyContext.polly) return
|
|
67
|
+
await pollyGlobals.pollyContext.polly.stop()
|
|
68
|
+
pollyGlobals.pollyContext.polly = null
|
|
69
|
+
})
|
|
70
|
+
},
|
|
63
71
|
},
|
|
64
72
|
}
|
package/src/jest.js
CHANGED
|
@@ -81,7 +81,10 @@ const makeEach =
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const
|
|
84
|
+
const execArgv = process.env.EXODUS_TEST_EXECARGV
|
|
85
|
+
? JSON.parse(process.env.EXODUS_TEST_EXECARGV)
|
|
86
|
+
: process.execArgv
|
|
87
|
+
const forceExit = execArgv.map((x) => x.replaceAll('_', '-')).includes('--test-force-exit')
|
|
85
88
|
|
|
86
89
|
const inConcurrent = []
|
|
87
90
|
const inDescribe = []
|
|
@@ -149,26 +152,29 @@ node.afterEach(() => {
|
|
|
149
152
|
for (const { error } of expect.extractExpectedAssertionsErrors()) throw error
|
|
150
153
|
})
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
if (globalThis.process) {
|
|
156
|
+
node.after(() => {
|
|
157
|
+
jestTimers.useRealTimers()
|
|
158
|
+
const prefix = `Tests completed, but still have asynchronous activity after`
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
setTimeout(() => {
|
|
159
|
-
console.error(`${prefix} additional ${timeout}ms. Terminating with a failure...`)
|
|
160
|
-
process.exit(1)
|
|
161
|
-
}, timeout).unref()
|
|
162
|
-
|
|
163
|
-
// Warn after 5s that something is going on
|
|
164
|
-
const warnTimeout = 5000
|
|
165
|
-
if (warnTimeout < timeout + 1000) {
|
|
160
|
+
// give everything additional (configurable) defaultTimeout time to finish, otherwide fail
|
|
161
|
+
const timeout = defaultTimeout
|
|
166
162
|
setTimeout(() => {
|
|
167
|
-
console.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
console.error(`${prefix} additional ${timeout}ms. Terminating with a failure...`)
|
|
164
|
+
process.exit(1)
|
|
165
|
+
}, timeout).unref()
|
|
166
|
+
|
|
167
|
+
// Warn after 5s that something is going on
|
|
168
|
+
const warnTimeout = 5000
|
|
169
|
+
if (warnTimeout < timeout + 1000) {
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
console.warn(`${prefix} ${warnTimeout}ms. Waiting for ${timeout}ms to pass to finish...`)
|
|
172
|
+
}, warnTimeout).unref()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
}
|
|
171
176
|
|
|
177
|
+
const isBundle = process.env.EXODUS_TEST_ENVIRONMENT === 'bundle' // TODO: improve mocking from bundle
|
|
172
178
|
export const jest = {
|
|
173
179
|
exodus: {
|
|
174
180
|
__proto__: null,
|
|
@@ -177,9 +183,9 @@ export const jest = {
|
|
|
177
183
|
platform: String(process.env.EXODUS_TEST_PLATFORM),
|
|
178
184
|
engine: String(node.engine),
|
|
179
185
|
timers: Boolean(mock.timers && haveValidTimers),
|
|
180
|
-
esmMocks: Boolean(mock.module), // full support for ESM mocks
|
|
181
|
-
|
|
182
|
-
esmNamedBuiltinMocks: Boolean(mock.module || insideEsbuild), // support for named ESM imports from builtin module mocks
|
|
186
|
+
esmMocks: Boolean(mock.module && !isBundle), // full support for ESM mocks
|
|
187
|
+
esmInterop: Boolean(insideEsbuild && !isBundle), // loading/using ESM as CJS, ESM mocks creation without a mocker function
|
|
188
|
+
esmNamedBuiltinMocks: Boolean(mock.module || (insideEsbuild && !isBundle)), // support for named ESM imports from builtin module mocks
|
|
183
189
|
concurrency: node.engine !== 'pure', // pure engine doesn't support concurrency
|
|
184
190
|
},
|
|
185
191
|
},
|
package/src/jest.mock.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mock,
|
|
3
3
|
assert,
|
|
4
|
-
|
|
4
|
+
requireIsRelative,
|
|
5
5
|
relativeRequire as require,
|
|
6
6
|
isTopLevelESM,
|
|
7
7
|
builtinModules,
|
|
@@ -34,7 +34,7 @@ export const jestModuleMocks = {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function resolveModule(name) {
|
|
37
|
-
assert(
|
|
37
|
+
assert(requireIsRelative || /^[@a-zA-Z]/u.test(name), 'Mocking relative paths is not possible')
|
|
38
38
|
const unprefixed = name.replace(/^node:/, '')
|
|
39
39
|
if (builtinModules.includes(unprefixed)) return unprefixed
|
|
40
40
|
return require.resolve(name)
|
|
@@ -58,6 +58,7 @@ export function resetModules() {
|
|
|
58
58
|
if (mock.module) ctx.restore()
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
assert(process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle', 'resetModules() unsupported from bundle')
|
|
61
62
|
for (const resolved of Object.keys(require.cache)) {
|
|
62
63
|
delete require.cache[resolved]
|
|
63
64
|
mapMocks.delete(resolved)
|
|
@@ -132,7 +133,9 @@ function mockCloneItem(obj, cache) {
|
|
|
132
133
|
// Special path, as .default might be a getter and we want to unwrap it
|
|
133
134
|
if (obj.__esModule === true) {
|
|
134
135
|
const { __esModule, default: def, ...rest } = obj
|
|
135
|
-
|
|
136
|
+
const proto = Object.getPrototypeOf(obj)
|
|
137
|
+
const toClone = proto?.[Symbol.toStringTag] === 'Module' ? proto : { default: def, ...rest } // unwrap bun modules for proper cloning
|
|
138
|
+
return { __esModule, ...mockClone(toClone, cache) }
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
const prototype = Object.getPrototypeOf(obj)
|
|
@@ -170,6 +173,8 @@ function mockCloneItem(obj, cache) {
|
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
export function jestmock(name, mocker, { override = false } = {}) {
|
|
176
|
+
assert(process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle', 'module mocks unsupported from bundle') // TODO: can we do something?
|
|
177
|
+
|
|
173
178
|
// Loaded ESM: isn't mocked
|
|
174
179
|
// Loaded CJS: mocked via object overriding
|
|
175
180
|
// Loaded built-ins: mocked via object overriding where possible
|