@exodus/test 1.0.0-rc.70 → 1.0.0-rc.71
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/bundler/bundle.js +126 -8
- package/bundler/modules/globals.cjs +43 -2
- package/bundler/modules/module.cjs +16 -0
- package/bundler/modules/util-format.cjs +1 -1
- package/bundler/modules/util.cjs +4 -0
- package/package.json +9 -4
- package/src/dark.cjs +70 -67
- package/src/jest.js +3 -2
- package/src/jest.mock.js +66 -8
package/bundler/bundle.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { readFile, readdir } from 'node:fs/promises'
|
|
2
|
+
import { readFile, writeFile, readdir } from 'node:fs/promises'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
5
|
import { basename, dirname, extname, resolve, join, relative } from 'node:path'
|
|
@@ -11,6 +11,8 @@ import glob from 'fast-glob'
|
|
|
11
11
|
const require = createRequire(import.meta.url)
|
|
12
12
|
const resolveRequire = (query) => require.resolve(query)
|
|
13
13
|
const resolveImport = import.meta.resolve && ((query) => fileURLToPath(import.meta.resolve(query)))
|
|
14
|
+
const cjsMockRegex = /\.exodus-test-mock\.cjs$/u
|
|
15
|
+
const cjsMockFallback = `throw new Error('Mocking loaded ESM modules in not possible in bundles')`
|
|
14
16
|
|
|
15
17
|
const readSnapshots = async (files, resolvers) => {
|
|
16
18
|
const snapshots = []
|
|
@@ -28,12 +30,42 @@ const readSnapshots = async (files, resolvers) => {
|
|
|
28
30
|
return snapshots
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
const stringify = (x) => ([undefined, null].includes(x) ? `${x}` : JSON.stringify(x))
|
|
31
34
|
const loadPipeline = [
|
|
32
35
|
function (source, filepath) {
|
|
33
|
-
|
|
36
|
+
let res = source
|
|
34
37
|
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(pathToFileURL(filepath)))
|
|
35
38
|
.replace(/\b(__dirname|import\.meta\.dirname)\b/g, JSON.stringify(dirname(filepath)))
|
|
36
39
|
.replace(/\b(__filename|import\.meta\.filename)\b/g, JSON.stringify(filepath))
|
|
40
|
+
|
|
41
|
+
// Unneded polyfills
|
|
42
|
+
for (const [a, b] of Object.entries({
|
|
43
|
+
'is-nan': 'Number.isNaN', // https://www.npmjs.com/package/is-nan description: ES2015-compliant shim for Number.isNaN
|
|
44
|
+
'is-nan/polyfill': '() => Number.isNaN',
|
|
45
|
+
'object.assign': 'Object.assign',
|
|
46
|
+
'object.assign/polyfill': '() => Object.assign',
|
|
47
|
+
'object-is': 'Object.is',
|
|
48
|
+
'object-is/polyfill': '() => Object.is',
|
|
49
|
+
hasown: 'Object.hasOwn',
|
|
50
|
+
gopd: 'Object.getOwnPropertyDescriptor',
|
|
51
|
+
'has-property-descriptors': '() => true',
|
|
52
|
+
'has-symbols': '() => true',
|
|
53
|
+
'has-symbols/shams': '() => true',
|
|
54
|
+
'has-tostringtag': "() => typeof Symbol.toStringTag === 'symbol'",
|
|
55
|
+
'has-tostringtag/shams': '() => !!Symbol.toStringTag',
|
|
56
|
+
'es-define-property': 'Object.defineProperty',
|
|
57
|
+
'es-errors': 'Error',
|
|
58
|
+
'es-errors/eval': 'EvalError',
|
|
59
|
+
'es-errors/range': 'RangeError',
|
|
60
|
+
'es-errors/ref': 'ReferenceError',
|
|
61
|
+
'es-errors/syntax': 'SyntaxError',
|
|
62
|
+
'es-errors/type': 'TypeError',
|
|
63
|
+
'es-errors/uri': 'URIError',
|
|
64
|
+
})) {
|
|
65
|
+
res = res.replaceAll(`require('${a}')`, `(${b})`).replaceAll(`require("${a}")`, `(${b})`) // Assumes well-formed names/code
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return res
|
|
37
69
|
},
|
|
38
70
|
]
|
|
39
71
|
|
|
@@ -98,7 +130,7 @@ const loadCache = new Map()
|
|
|
98
130
|
const loadSourceFileBase = async (filepath) => {
|
|
99
131
|
if (!loadCache.has(filepath)) {
|
|
100
132
|
const load = async () => {
|
|
101
|
-
let contents = await readFile(filepath, 'utf8')
|
|
133
|
+
let contents = await readFile(filepath.replace(cjsMockRegex, ''), 'utf8')
|
|
102
134
|
for (const transform of loadPipeline) contents = await transform(contents, filepath)
|
|
103
135
|
return contents
|
|
104
136
|
}
|
|
@@ -124,7 +156,6 @@ export const build = async (...files) => {
|
|
|
124
156
|
const input = []
|
|
125
157
|
const importSource = async (file) => input.push(await loadSourceFile(resolveRequire(file)))
|
|
126
158
|
const importFile = (...args) => input.push(`await import(${JSON.stringify(resolve(...args))});`)
|
|
127
|
-
const stringify = (x) => ([undefined, null].includes(x) ? `${x}` : JSON.stringify(x))
|
|
128
159
|
|
|
129
160
|
if (!['node', 'electron'].includes(options.platform)) {
|
|
130
161
|
if (process.env.EXODUS_TEST_IS_BAREBONE) {
|
|
@@ -182,7 +213,7 @@ export const build = async (...files) => {
|
|
|
182
213
|
const fsFilesDirs = new Map()
|
|
183
214
|
const cwd = process.cwd()
|
|
184
215
|
const fixturesRegex = /(fixtures|samples)/u
|
|
185
|
-
const aggressiveExtensions = /\.(json|txt|hex)$/u // These are bundled when just used in path.join and by wildcard from fixtures/
|
|
216
|
+
const aggressiveExtensions = /\.(json|txt|hex|wasm)$/u // These are bundled when just used in path.join and by wildcard from fixtures/
|
|
186
217
|
const fileAllowed = (f) =>
|
|
187
218
|
f && f.startsWith(`${cwd}/`) && resolve(f) === f && /^[a-z0-9@_./-]+$/iu.test(relative(cwd, f))
|
|
188
219
|
|
|
@@ -241,7 +272,28 @@ export const build = async (...files) => {
|
|
|
241
272
|
if (/(readdir|readFile|exists)Sync/u.test(source)) await fsFilesBundleFixtures('fs')
|
|
242
273
|
if (fixturesRegex.test(source)) await fsFilesBundleFixtures('fixtures')
|
|
243
274
|
|
|
244
|
-
|
|
275
|
+
// Resolve require.resolve and bundle those files for fixture or json extensions (e.g. package.json)
|
|
276
|
+
let filepathRequire
|
|
277
|
+
const toAdd = []
|
|
278
|
+
const res = source.replace(
|
|
279
|
+
/\b(require|import\.meta)\.resolve\(\s*(?:"([^"\\]+)"|'([^'\\]+)')\s*\)/gu,
|
|
280
|
+
(orig, cause, a, b) => {
|
|
281
|
+
if (!filepathRequire) filepathRequire = createRequire(filepath)
|
|
282
|
+
try {
|
|
283
|
+
const file = filepathRequire.resolve(a || b)
|
|
284
|
+
if (aggressiveExtensions.test(file)) toAdd.push(file) // load resolved files for specific extensions
|
|
285
|
+
if (cause === 'require') return `(${stringify(file)})`
|
|
286
|
+
// Do not replace import.meta.resolve for non-fixture extensions, might cause misresolutions
|
|
287
|
+
return aggressiveExtensions.test(file) ? `(${stringify(pathToFileURL(file))})` : orig
|
|
288
|
+
} catch {
|
|
289
|
+
return orig
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
for (const file of toAdd) await fsFilesAdd(file)
|
|
295
|
+
|
|
296
|
+
return res
|
|
245
297
|
})
|
|
246
298
|
|
|
247
299
|
if (files.length === 1) {
|
|
@@ -266,6 +318,7 @@ export const build = async (...files) => {
|
|
|
266
318
|
'fs/promises': api('fs-promises.cjs'),
|
|
267
319
|
http: api('http.cjs'),
|
|
268
320
|
https: api('https.cjs'),
|
|
321
|
+
module: api('module.cjs'),
|
|
269
322
|
os: resolveRequire('os-browserify/browser.js'), // 'main' entry point is noop, we want browser entry
|
|
270
323
|
path: resolveRequire('path-browserify'),
|
|
271
324
|
querystring: resolveRequire('querystring-es3'),
|
|
@@ -273,7 +326,7 @@ export const build = async (...files) => {
|
|
|
273
326
|
timers: resolveRequire('timers-browserify'),
|
|
274
327
|
tty: api('tty.cjs'),
|
|
275
328
|
url: api('url.cjs'),
|
|
276
|
-
util:
|
|
329
|
+
util: api('util.cjs'),
|
|
277
330
|
zlib: resolveRequire('browserify-zlib'),
|
|
278
331
|
}
|
|
279
332
|
|
|
@@ -294,6 +347,7 @@ export const build = async (...files) => {
|
|
|
294
347
|
'process.stderr': 'undefined',
|
|
295
348
|
'process.stdout': 'undefined',
|
|
296
349
|
'process.type': 'undefined',
|
|
350
|
+
'process.platform': 'undefined',
|
|
297
351
|
'process.version': stringify('v22.5.1'), // shouldn't depend on currently used Node.js version
|
|
298
352
|
'process.versions.node': stringify('22.5.1'), // see line above
|
|
299
353
|
EXODUS_TEST_PROCESS_CWD: stringify(process.cwd()),
|
|
@@ -318,6 +372,7 @@ export const build = async (...files) => {
|
|
|
318
372
|
...nodeUnprefixed,
|
|
319
373
|
// Needed for polyfills but name conflicts with Node.js modules
|
|
320
374
|
'url/url.js': resolveRequire('url/url.js'),
|
|
375
|
+
'util/util.js': resolveRequire('util/util.js'),
|
|
321
376
|
// expect-related deps
|
|
322
377
|
'ansi-styles': api('ansi-styles.cjs'),
|
|
323
378
|
'jest-util': api('jest-util.js'),
|
|
@@ -339,7 +394,12 @@ export const build = async (...files) => {
|
|
|
339
394
|
plugins: [
|
|
340
395
|
{
|
|
341
396
|
name: 'exodus-test.bundle',
|
|
342
|
-
setup({ onLoad }) {
|
|
397
|
+
setup({ onResolve, onLoad }) {
|
|
398
|
+
onResolve({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
|
|
399
|
+
if (shouldInstallMocks && cjsMockRegex.test(args.path)) {
|
|
400
|
+
return { path: args.path, namespace: 'file' }
|
|
401
|
+
}
|
|
402
|
+
})
|
|
343
403
|
onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: 'file' }, async (args) => {
|
|
344
404
|
let filepath = args.path
|
|
345
405
|
// Resolve .native versions
|
|
@@ -359,6 +419,46 @@ export const build = async (...files) => {
|
|
|
359
419
|
],
|
|
360
420
|
}
|
|
361
421
|
|
|
422
|
+
let shouldInstallMocks = false
|
|
423
|
+
const mocked = new Set()
|
|
424
|
+
specificLoadPipeline.push(async (source, filepath) => {
|
|
425
|
+
if (shouldInstallMocks) {
|
|
426
|
+
if (cjsMockRegex.test(filepath)) return cjsMockFallback
|
|
427
|
+
if (mocked.has(filepath) && !filepath.endsWith('.cjs') && /^export\b/mu.test(source)) {
|
|
428
|
+
const mock = stringify(`${filepath}.exodus-test-mock.cjs`)
|
|
429
|
+
const def = 'x.__esModule ? x.default : (x.default ?? x)'
|
|
430
|
+
return `export * from ${mock}\nvar x = require(${mock})\nexport default ${def}`
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 'await import' is replaced only in files with mocks (likely toplevel there)
|
|
435
|
+
// Otherwise we don't patch module system at all
|
|
436
|
+
if (!source.includes('jest.doMock') && !source.includes('jest.mock')) return source
|
|
437
|
+
shouldInstallMocks = true
|
|
438
|
+
const filepathRequire = createRequire(filepath)
|
|
439
|
+
return source
|
|
440
|
+
.replaceAll(/\bawait (import\((?:"[^"\\]+"|'[^'\\]+')\))/gu, 'EXODUS_TEST_SYNC_IMPORT($1)')
|
|
441
|
+
.replaceAll(
|
|
442
|
+
/\bjest\.(doMock|mock|requireActual|requireMock)\(\s*("[^"\\]+"|'[^'\\]+')/gu,
|
|
443
|
+
(_, method, raw) => {
|
|
444
|
+
try {
|
|
445
|
+
const arg = JSON.parse(raw[0] === "'" ? raw.replaceAll("'", '"') : raw) // fine because it doesn't have quotes or \
|
|
446
|
+
const { alias } = config
|
|
447
|
+
const file = Object.hasOwn(alias, arg) ? alias[arg] : filepathRequire.resolve(arg) // throws when not resolved
|
|
448
|
+
assert(existsSync(file), `File ${file} does not exist`)
|
|
449
|
+
const builtin = stringify(Object.hasOwn(alias, arg) ? arg.replace(/^node:/, '') : null)
|
|
450
|
+
const id = `bundle:${relative(cwd, file)}`
|
|
451
|
+
if (method.startsWith('require')) return `jest.${method}(${stringify(id)}`
|
|
452
|
+
mocked.add(file)
|
|
453
|
+
return `jest.__${method}Bundle(${stringify(id)},${builtin},()=>require(${raw})`
|
|
454
|
+
} catch (err) {
|
|
455
|
+
console.error(err)
|
|
456
|
+
throw new Error(`Failed to mock ${raw}: not resolved`, { cause: err })
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
})
|
|
461
|
+
|
|
362
462
|
if (files.length === 1) {
|
|
363
463
|
config.define['process.argv'] = stringify(['exodus-test', resolve(files[0])])
|
|
364
464
|
}
|
|
@@ -379,6 +479,24 @@ export const build = async (...files) => {
|
|
|
379
479
|
assert.equal(res instanceof Error, res.errors.length > 0)
|
|
380
480
|
}
|
|
381
481
|
|
|
482
|
+
if (res.errors.length === 0 && shouldInstallMocks) {
|
|
483
|
+
const code = await readFile(outfile, 'utf8')
|
|
484
|
+
const heads = {
|
|
485
|
+
esm: /(var __esm = (?:function)?\((fn[\d]*), res[\d]*\)\s*(?:=>|\{\s*return)\s*)(function __init[\d]*\(\) \{)/u,
|
|
486
|
+
cjs: /(var __commonJS = (?:function)?\((cb[\d]*), mod[\d]*\)\s*(?:=>|\{\s*return)\s*)(function __require[\d]*\(\) \{)/u,
|
|
487
|
+
}
|
|
488
|
+
const k = '__getOwnPropNames($2)[0]'
|
|
489
|
+
const mock = (p, l, v) =>
|
|
490
|
+
`var ${p}=new Set(),${l}=new Set(),${v}=new Map();$1${p}.add(${k}) && $3;{const k=${k};${l}.add(k);if (${v}.has(k))return ${v}.get(k)};`
|
|
491
|
+
assert(heads.esm.test(code) && heads.cjs.test(code), 'Failed to match for module mocks')
|
|
492
|
+
const patched = code
|
|
493
|
+
.replace(heads.esm, mock('__mocksESMPossible', '__mocksESMLoaded', '__mocksESM')) // __mocksESM actually doesn't work
|
|
494
|
+
.replace(heads.cjs, mock('__mocksCJSPossible', '__mocksCJSLoaded', '__mocksCJS'))
|
|
495
|
+
.replaceAll('EXODUS_TEST_SYNC_IMPORT(Promise.resolve().then(', '((f=>f())(')
|
|
496
|
+
assert(!patched.includes('EXODUS_TEST_SYNC_IMPORT'), "Failed to fix 'await import'")
|
|
497
|
+
await writeFile(outfile, patched)
|
|
498
|
+
}
|
|
499
|
+
|
|
382
500
|
// if (res.errors.length === 0) require('fs').copyFileSync(outfile, 'tempout.cjs') // DEBUG
|
|
383
501
|
|
|
384
502
|
// We treat warnings as errors, so just merge all them
|
|
@@ -5,6 +5,7 @@ if (!globalThis.global) globalThis.global = globalThis
|
|
|
5
5
|
if (!globalThis.Buffer) globalThis.Buffer = require('buffer').Buffer
|
|
6
6
|
|
|
7
7
|
const consoleKeys = ['log', 'error', 'warn', 'info', 'debug', 'trace']
|
|
8
|
+
const { print } = globalThis
|
|
8
9
|
if (!globalThis.console) globalThis.console = Object.fromEntries(consoleKeys.map((k) => [k, print])) // eslint-disable-line no-undef
|
|
9
10
|
|
|
10
11
|
// In browsers e.g. errors (and some other objects) are hard to unwrap via the API
|
|
@@ -12,6 +13,7 @@ if (!globalThis.console) globalThis.console = Object.fromEntries(consoleKeys.map
|
|
|
12
13
|
// In barebone, we don't want console.log({x:10}) to print "[Object object]"", we want "{ x: 10 }"
|
|
13
14
|
if (process.env.EXODUS_TEST_IS_BROWSER || process.env.EXODUS_TEST_IS_BAREBONE) {
|
|
14
15
|
const utilFormat = require('exodus-test:util-format')
|
|
16
|
+
if (print) globalThis.print = (...args) => print(utilFormat(...args))
|
|
15
17
|
for (const type of consoleKeys) {
|
|
16
18
|
if (!Object.hasOwn(console, type)) continue
|
|
17
19
|
const orig = console[type].bind(console)
|
|
@@ -49,10 +51,12 @@ if (!globalThis.WebSocket) {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
if (!Array.prototype.at) {
|
|
52
|
-
|
|
53
|
-
Array.prototype.at = function (i) {
|
|
54
|
+
const at = function (i) {
|
|
54
55
|
return this[i < 0 ? this.length + i : i]
|
|
55
56
|
}
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line no-extend-native
|
|
59
|
+
Object.defineProperty(Array.prototype, 'at', { configurable: true, writable: true, value: at })
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
if (process.env.EXODUS_TEST_PLATFORM === 'hermes') {
|
|
@@ -66,6 +70,43 @@ if (process.env.EXODUS_TEST_PLATFORM === 'hermes') {
|
|
|
66
70
|
)
|
|
67
71
|
Promise.allSettled = (iterable) => Promise.all([...iterable].map((element) => wrap(element)))
|
|
68
72
|
}
|
|
73
|
+
|
|
74
|
+
// Refs: https://github.com/facebook/hermes/commit/e97db61b49bd0c065a3ce7da46f074bc39b80c6a
|
|
75
|
+
if (!Promise.any) {
|
|
76
|
+
const AggregateError =
|
|
77
|
+
globalThis.AggregateError ||
|
|
78
|
+
class AggregateError extends Error {
|
|
79
|
+
constructor(errors, message) {
|
|
80
|
+
super(message)
|
|
81
|
+
this.name = 'AggregateError'
|
|
82
|
+
this.errors = errors
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const errmsg = 'All promises were rejected'
|
|
87
|
+
Promise.any = function (values) {
|
|
88
|
+
const promises = [...values]
|
|
89
|
+
const errors = []
|
|
90
|
+
if (promises.length === 0) return Promise.reject(new AggregateError(errors, errmsg))
|
|
91
|
+
let resolved = false
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const oneResolve = (value) => {
|
|
94
|
+
if (resolved) return
|
|
95
|
+
resolved = true
|
|
96
|
+
errors.length = 0
|
|
97
|
+
resolve(value)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const oneReject = (error) => {
|
|
101
|
+
if (resolved) return
|
|
102
|
+
errors.push(error)
|
|
103
|
+
if (errors.length === promises.length) reject(new AggregateError(errors, errmsg))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
promises.forEach((promise) => Promise.resolve(promise).then(oneResolve, oneReject))
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
if (globalThis.describe) delete globalThis.describe
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const ids = 'Module,SourceMap,builtinModules,findSourceMap,globalPaths,isBuiltin,runMain'.split(',')
|
|
2
|
+
|
|
3
|
+
const makeMethod = (key) => {
|
|
4
|
+
// Not an arrow as there are classes that can be called with new
|
|
5
|
+
return function () {
|
|
6
|
+
throw new Error(`module.${key} unsupported in bundled mode`)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const createRequire = (filename) => (file) => {
|
|
11
|
+
const clean = file.replace(/^node:/, '')
|
|
12
|
+
if (globalThis.EXODUS_TEST_MOCK_BUILTINS?.has(clean)) return EXODUS_TEST_MOCK_BUILTINS.get(clean) // eslint-disable-line no-undef
|
|
13
|
+
throw new Error(`module.createRequire is unsupported in bundled mode (origin: ${filename})`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { ...Object.fromEntries(ids.map((key) => [key, makeMethod(key)])), createRequire }
|
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.71",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -58,10 +58,12 @@
|
|
|
58
58
|
"bundler/modules/globals.cjs",
|
|
59
59
|
"bundler/modules/jest-message-util.js",
|
|
60
60
|
"bundler/modules/jest-util.js",
|
|
61
|
+
"bundler/modules/module.cjs",
|
|
61
62
|
"bundler/modules/node-buffer.cjs",
|
|
62
63
|
"bundler/modules/text-encoding-utf.cjs",
|
|
63
64
|
"bundler/modules/tty.cjs",
|
|
64
65
|
"bundler/modules/url.cjs",
|
|
66
|
+
"bundler/modules/util.cjs",
|
|
65
67
|
"bundler/modules/util-format.cjs",
|
|
66
68
|
"bundler/modules/ws.cjs",
|
|
67
69
|
"loaders/babel.cjs",
|
|
@@ -96,8 +98,8 @@
|
|
|
96
98
|
],
|
|
97
99
|
"scripts": {
|
|
98
100
|
"test:_pure": "EXODUS_TEST_IGNORE='tests/jest-extended/**' npm run test",
|
|
99
|
-
"test:_bundle": "EXODUS_TEST_IGNORE='tests/{{jest-extended,inband}/**,jest-when/when.test.*,jest/jest.resetModules
|
|
100
|
-
"test": "npm run test:jest",
|
|
101
|
+
"test:_bundle": "EXODUS_TEST_IGNORE='tests/{{jest-extended,inband}/**,jest-when/when.test.*,jest/jest.resetModules.*}' npm run test --",
|
|
102
|
+
"test": "npm run test:jest --",
|
|
101
103
|
"test:all": "npm run test:jest && npm run test:tape && npm run test:native && npm run test:pure && npm run test:typescript && npm run test:fetch && npm run test:jsdom",
|
|
102
104
|
"test:native": "EXODUS_TEST_IGNORE='{**/typescript/**,**/jest-repo/**/user.test.js}' ./bin/index.js --jest 'tests/**/*.test.{js,cjs,mjs}'",
|
|
103
105
|
"test:typescript": "./bin/index.js --jest --typescript tests/typescript.test.ts",
|
|
@@ -113,7 +115,10 @@
|
|
|
113
115
|
"test:chromium:playwright": "EXODUS_TEST_ENGINE=chromium:playwright npm run test:_bundle --",
|
|
114
116
|
"test:firefox:playwright": "EXODUS_TEST_ENGINE=firefox:playwright npm run test:_bundle --",
|
|
115
117
|
"test:webkit:playwright": "EXODUS_TEST_ENGINE=webkit:playwright npm run test:_bundle --",
|
|
116
|
-
"test:
|
|
118
|
+
"test:bundle": "EXODUS_TEST_ENGINE=node:bundle npm run test:_bundle --",
|
|
119
|
+
"test:d8": "EXODUS_TEST_ENGINE=d8:bundle npm run test:_bundle --",
|
|
120
|
+
"test:jsc": "EXODUS_TEST_ENGINE=jsc:bundle npm run test:_bundle --",
|
|
121
|
+
"test:hermes": "EXODUS_TEST_ENGINE=hermes:bundle npm run test:_bundle --",
|
|
117
122
|
"test:fetch": "./bin/index.js --jest --drop-network --engine node:pure tests/fetch.test.js tests/websocket.test.js",
|
|
118
123
|
"test:jsdom": "EXODUS_TEST_JEST_CONFIG='{\"testMatch\":[\"**/*.jsdom-test.js\"],\"testEnvironment\":\"jsdom\", \"rootDir\": \".\"}' ./bin/index.js --jest",
|
|
119
124
|
"coverage": "./bin/index.js --jest --esbuild --coverage",
|
package/src/dark.cjs
CHANGED
|
@@ -19,33 +19,34 @@ let getCallerLocation
|
|
|
19
19
|
|
|
20
20
|
function createCallerLocationHook() {
|
|
21
21
|
if (getCallerLocation) return { installLocationInNextTest, getCallerLocation }
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
locForNextTest
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
22
|
+
getCallerLocation = () => {}
|
|
23
|
+
|
|
24
|
+
if (process.env.EXODUS_TEST_ENGINE === 'node:test') {
|
|
25
|
+
try {
|
|
26
|
+
const { Test } = require('node:internal/test_runner/test')
|
|
27
|
+
const { fileURLToPath } = require('node:url')
|
|
28
|
+
const mayBeUrlToPath = (str) => (str.startsWith('file://') ? fileURLToPath(str) : str)
|
|
29
|
+
const locStorage = new Map()
|
|
30
|
+
Object.defineProperty(Test.prototype, 'loc', {
|
|
31
|
+
get() {
|
|
32
|
+
return locStorage.get(this)
|
|
33
|
+
},
|
|
34
|
+
set(val) {
|
|
35
|
+
locStorage.set(this, val)
|
|
36
|
+
if (locForNextTest) {
|
|
37
|
+
const loc = locForNextTest
|
|
38
|
+
locForNextTest = undefined
|
|
39
|
+
locStorage.set(this, { line: loc[0], column: loc[1], file: mayBeUrlToPath(loc[2]) })
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// We can replicate getCallerLocation() with public V8 Error CallSite API, but we won't
|
|
45
|
+
// need it anyway if we don't have a path for hook into internal Test implementation
|
|
46
|
+
|
|
47
|
+
const { internalBinding } = require('node:internal/test/binding')
|
|
48
|
+
getCallerLocation = internalBinding('util').getCallerLocation
|
|
49
|
+
} catch {}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
return { installLocationInNextTest, getCallerLocation }
|
|
@@ -56,51 +57,53 @@ function getTestNamePath(t) {
|
|
|
56
57
|
// No implementation in Node.js yet, will have to PR
|
|
57
58
|
if (t.fullName) return t.fullName.split(' > ')
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
if (process.env.EXODUS_TEST_ENGINE === 'node:test') {
|
|
61
|
+
// We are on Node.js < 22.3.0 where even t.fullName doesn't exist yet, polyfill
|
|
62
|
+
const namePath = Symbol('namePath')
|
|
63
|
+
const getNamePath = Symbol('getNamePath')
|
|
64
|
+
try {
|
|
65
|
+
if (t[namePath]) return t[namePath]
|
|
66
|
+
|
|
67
|
+
// Sigh, ok, whatever
|
|
68
|
+
const { Test } = require('node:internal/test_runner/test')
|
|
69
|
+
|
|
70
|
+
const usePathName = Symbol('usePathName')
|
|
71
|
+
const restoreName = Symbol('restoreName')
|
|
72
|
+
Test.prototype[getNamePath] = function () {
|
|
73
|
+
if (this === this.root) return []
|
|
74
|
+
return [...(this.parent?.[getNamePath]() || []), this.name]
|
|
75
|
+
}
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
const diagnostic = Test.prototype.diagnostic
|
|
78
|
+
Test.prototype.diagnostic = function (...args) {
|
|
79
|
+
if (args[0] === usePathName) {
|
|
80
|
+
this[restoreName] = this.name
|
|
81
|
+
this.name = this[getNamePath]()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
81
|
-
}
|
|
85
|
+
if (args[0] === restoreName) {
|
|
86
|
+
this.name = this[restoreName]
|
|
87
|
+
delete this[restoreName]
|
|
88
|
+
return
|
|
89
|
+
}
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
this.name = this[restoreName]
|
|
85
|
-
delete this[restoreName]
|
|
86
|
-
return
|
|
91
|
+
return diagnostic.apply(this, args)
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return t[namePath]
|
|
103
|
-
} catch {}
|
|
94
|
+
const TestContextProto = Object.getPrototypeOf(t)
|
|
95
|
+
Object.defineProperty(TestContextProto, namePath, {
|
|
96
|
+
get() {
|
|
97
|
+
this.diagnostic(usePathName)
|
|
98
|
+
const result = this.name
|
|
99
|
+
this.diagnostic(restoreName)
|
|
100
|
+
return result
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return t[namePath]
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
104
107
|
|
|
105
108
|
return [t.name] // last resort
|
|
106
109
|
}
|
package/src/jest.js
CHANGED
|
@@ -222,9 +222,10 @@ export const jest = {
|
|
|
222
222
|
features: {
|
|
223
223
|
__proto__: null,
|
|
224
224
|
timers: Boolean(mock.timers && haveValidTimers),
|
|
225
|
-
|
|
225
|
+
dynamicRequire: Boolean(!isBundle), // require(non-literal-non-glob), createRequire()(non-builtin)
|
|
226
|
+
esmMocks: Boolean(mock.module || isBundle), // support for ESM mocks
|
|
226
227
|
esmInterop: Boolean(insideEsbuild && !isBundle), // loading/using ESM as CJS, ESM mocks creation without a mocker function
|
|
227
|
-
esmNamedBuiltinMocks: Boolean(mock.module ||
|
|
228
|
+
esmNamedBuiltinMocks: Boolean(mock.module || insideEsbuild || isBundle), // support for named ESM imports from builtin module mocks
|
|
228
229
|
concurrency: node.engine !== 'pure', // pure engine doesn't support concurrency
|
|
229
230
|
},
|
|
230
231
|
mock: {
|
package/src/jest.mock.js
CHANGED
|
@@ -34,7 +34,38 @@ export const jestModuleMocks = {
|
|
|
34
34
|
resetModules,
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
38
|
+
globalThis.EXODUS_TEST_MOCK_BUILTINS = new Map()
|
|
39
|
+
Object.assign(jestModuleMocks, {
|
|
40
|
+
__mockBundle: wrap((name, builtin, actual, mock) =>
|
|
41
|
+
jestmock(name, mock, { actual, builtin, override: true })
|
|
42
|
+
),
|
|
43
|
+
__doMockBundle: wrap((name, builtin, actual, mock) =>
|
|
44
|
+
jestmock(name, mock, { actual, builtin })
|
|
45
|
+
),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// For bundles
|
|
50
|
+
const cjsSet = typeof __mocksCJSPossible === 'undefined' ? null : __mocksCJSPossible // eslint-disable-line no-undef
|
|
51
|
+
const esmSet = typeof __mocksESMPossible === 'undefined' ? null : __mocksESMPossible // eslint-disable-line no-undef
|
|
52
|
+
|
|
37
53
|
function resolveModule(name) {
|
|
54
|
+
if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
55
|
+
assert(name.startsWith('bundle:'), `Can't mock unresolved ${name} in bundle, use static syntax`)
|
|
56
|
+
assert(cjsSet && esmSet, 'Module mocking not installed correctly in bundle')
|
|
57
|
+
const id = name.replace(/^bundle:/u, '')
|
|
58
|
+
assert(!cjsSet?.has(id) || !esmSet?.has(id), 'CJS/ESM conflict in bundle mock')
|
|
59
|
+
assert(cjsSet?.has(id) || esmSet?.has(id), `Mock: can not find ${id} in bundle. Unused mock?`)
|
|
60
|
+
const cjs = `${id}.exodus-test-mock.cjs`
|
|
61
|
+
if (esmSet.has(id) && cjsSet.has(cjs)) {
|
|
62
|
+
assert(!esmSet.has(cjs))
|
|
63
|
+
return cjs
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return id
|
|
67
|
+
}
|
|
68
|
+
|
|
38
69
|
assert(requireIsRelative || /^[@a-zA-Z]/u.test(name), 'Mocking relative paths is not possible')
|
|
39
70
|
const unprefixed = name.replace(/^node:/, '')
|
|
40
71
|
if (builtinModules.includes(unprefixed)) return unprefixed
|
|
@@ -182,15 +213,16 @@ function mockCloneItem(obj, cache) {
|
|
|
182
213
|
return null
|
|
183
214
|
}
|
|
184
215
|
|
|
185
|
-
function jestmock(name, mocker, { override = false } = {}) {
|
|
186
|
-
assert(process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle', 'module mocks unsupported from bundle') // TODO: can we do something?
|
|
187
|
-
|
|
216
|
+
function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
|
|
188
217
|
// Loaded ESM: isn't mocked
|
|
189
218
|
// Loaded CJS: mocked via object overriding
|
|
190
219
|
// Loaded built-ins: mocked via object overriding where possible
|
|
191
|
-
// New CJS: mocked via mock.module + require.cache
|
|
192
|
-
// New ESM: mocked via mock.module
|
|
220
|
+
// New CJS, doMock CJS: mocked via mock.module + require.cache
|
|
221
|
+
// New ESM, doMock ESM: mocked via mock.module
|
|
193
222
|
// New built-ins: mocked via mock.module
|
|
223
|
+
// [Bundled] New CJS, doMock CJS: mocked via bundle hook
|
|
224
|
+
// [Bundled] New ESM, doMock ESM: isn't mocked
|
|
225
|
+
// [Bundled] New built-ins: mocked via bundle hook
|
|
194
226
|
|
|
195
227
|
const resolved = resolveModule(name)
|
|
196
228
|
assert(!mapMocks.has(resolved), 'Re-mocking the same module is not supported')
|
|
@@ -199,14 +231,24 @@ function jestmock(name, mocker, { override = false } = {}) {
|
|
|
199
231
|
'Built-in modules mocked with jest.mock can not be remocked, use jest.doMock'
|
|
200
232
|
)
|
|
201
233
|
|
|
234
|
+
let havePrior
|
|
235
|
+
if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
236
|
+
havePrior = __mocksCJSLoaded.has(resolved) || __mocksESMLoaded.has(resolved) // eslint-disable-line no-undef
|
|
237
|
+
assert(actual)
|
|
238
|
+
} else {
|
|
239
|
+
havePrior = Object.hasOwn(require.cache, resolved)
|
|
240
|
+
assert(!actual && !builtin)
|
|
241
|
+
}
|
|
242
|
+
|
|
202
243
|
// Attempt to load it
|
|
203
244
|
// Jest also loads modules on mock
|
|
204
245
|
// Can be ESM, so let it fail silently
|
|
205
|
-
const havePrior = Object.hasOwn(require.cache, resolved)
|
|
206
246
|
try {
|
|
207
|
-
|
|
247
|
+
assert(!resolved.endsWith('.exodus-test-mock.cjs')) // actual() would attempt to load non-wrapped ESM here
|
|
248
|
+
mapActual.set(resolved, actual ? actual() : require(resolved))
|
|
208
249
|
} catch {
|
|
209
|
-
|
|
250
|
+
const reason = actual ? 'in bundle' : 'without --esbuild or newer Node.js'
|
|
251
|
+
assert(mocker, `Can not auto-clone a native ESM module ${reason}`)
|
|
210
252
|
}
|
|
211
253
|
|
|
212
254
|
const expand = (obj) => (isObject(obj) ? { ...obj } : obj)
|
|
@@ -214,6 +256,22 @@ function jestmock(name, mocker, { override = false } = {}) {
|
|
|
214
256
|
mapMocks.set(resolved, value)
|
|
215
257
|
|
|
216
258
|
loadExpect() // we need to do this as we don't want mocks affecting expect
|
|
259
|
+
|
|
260
|
+
if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
261
|
+
if (builtin) globalThis.EXODUS_TEST_MOCK_BUILTINS.set(builtin, value)
|
|
262
|
+
if (havePrior && override) overrideModule(resolved) // This won't work on ESM
|
|
263
|
+
|
|
264
|
+
if (cjsSet?.has(resolved)) {
|
|
265
|
+
__mocksCJS.set(resolved, value) // eslint-disable-line no-undef
|
|
266
|
+
} else if (esmSet?.has(resolved)) {
|
|
267
|
+
throw new Error('ESM module mocks are not supported from bundle') // TODO: can we do something?
|
|
268
|
+
} else {
|
|
269
|
+
throw new Error('unreachable')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return this
|
|
273
|
+
}
|
|
274
|
+
|
|
217
275
|
const topLevelESM = isTopLevelESM()
|
|
218
276
|
let likelyESM = topLevelESM && !insideEsbuild && ![null, resolved].includes(resolveImport(name))
|
|
219
277
|
let okFromESM = false
|