@exodus/test 1.0.0-rc.97 → 1.0.0-rc.98
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 +3 -0
- package/loader/flow.js +17 -0
- package/loader/jest.js +2 -4
- package/loader/typescript.loader.js +2 -8
- package/package.json +5 -4
- package/src/jest.config.js +16 -7
- package/src/jest.mock.js +60 -44
- package/src/jest.setup.js +7 -0
package/bin/index.js
CHANGED
|
@@ -327,6 +327,9 @@ if (process.env.EXODUS_TEST_IGNORE) {
|
|
|
327
327
|
ignore.push(process.env.EXODUS_TEST_IGNORE)
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
// This might be used in presets, so has to be loaded before jest
|
|
331
|
+
if (options.flow && !options.bundle) args.push('--import', import.meta.resolve('../loader/flow.js'))
|
|
332
|
+
|
|
330
333
|
// The comment below is disabled, we don't auto-mock @jest/globals anymore, and having our loader first is faster
|
|
331
334
|
// [Disabled] Our loader should be last, as enabling module mocks confuses other loaders
|
|
332
335
|
let jestConfig = null
|
package/loader/flow.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { registerHooks } from 'node:module' // 22.15+
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import flowRemoveTypes from '@exodus/test-bundler/flow-remove-types'
|
|
4
|
+
|
|
5
|
+
function load(url, context, nextLoad) {
|
|
6
|
+
if (url.startsWith('file://')) {
|
|
7
|
+
const source = readFileSync(new URL(url), 'utf8')
|
|
8
|
+
if (source.includes('@flow')) {
|
|
9
|
+
const stripped = flowRemoveTypes(source, { pretty: true }).toString()
|
|
10
|
+
return { format: context.format || 'commonjs', source: stripped, shortCircuit: true }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return nextLoad(url, context)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
registerHooks({ load })
|
package/loader/jest.js
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
const {
|
|
2
|
-
await
|
|
3
|
-
const { should, ...jestGlobals } = await import('../src/jest.js') // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
4
|
-
await installJestEnvironment(jestGlobals)
|
|
1
|
+
const { setupJest } = await import('../src/jest.setup.js')
|
|
2
|
+
await setupJest()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { stripTypeScriptTypes } from 'node:module' // 22.13.0+
|
|
2
3
|
import { fileURLToPath } from 'node:url'
|
|
3
4
|
|
|
4
5
|
const extensionsRegex = /\.[cm]?ts$/
|
|
@@ -12,18 +13,11 @@ function shouldProcessUrl(s) {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
let transformSync
|
|
16
|
-
|
|
17
16
|
export async function load(url, context, nextLoad) {
|
|
18
17
|
if (shouldProcessUrl(url)) {
|
|
19
|
-
if (!transformSync) {
|
|
20
|
-
const amaro = await import('amaro')
|
|
21
|
-
transformSync = amaro.transformSync
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
const sourceBuf = await readFile(new URL(url))
|
|
25
19
|
const source = sourceBuf.toString('utf8')
|
|
26
|
-
const
|
|
20
|
+
const transformed = stripTypeScriptTypes(source, { mode: 'strip' })
|
|
27
21
|
const transformedBuf = Buffer.from(transformed)
|
|
28
22
|
if (sourceBuf.length !== transformedBuf.length) throw new Error('length mismatch')
|
|
29
23
|
// eslint-disable-next-line unicorn/no-for-loop
|
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.98",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"loader/babel.cjs",
|
|
49
49
|
"loader/esbuild.js",
|
|
50
50
|
"loader/esbuild.optional.js",
|
|
51
|
+
"loader/flow.js",
|
|
51
52
|
"loader/jest.js",
|
|
52
53
|
"loader/typescript.js",
|
|
53
54
|
"loader/typescript.loader.js",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"src/jest.environment.js",
|
|
67
68
|
"src/jest.fn.js",
|
|
68
69
|
"src/jest.mock.js",
|
|
70
|
+
"src/jest.setup.js",
|
|
69
71
|
"src/jest.snapshot.js",
|
|
70
72
|
"src/jest.timers.js",
|
|
71
73
|
"src/node.js",
|
|
@@ -84,7 +86,7 @@
|
|
|
84
86
|
"scripts": {
|
|
85
87
|
"test:_bundle": "EXODUS_TEST_IGNORE='tests/{{jest-extended,inband}/**,jest-when/when.test.*,jest/jest.resetModules.*,jest/mock/jest.mock.mocks-dir.test.js}' npm run test --",
|
|
86
88
|
"test": "npm run test:jest --",
|
|
87
|
-
"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:
|
|
89
|
+
"test:all": "npm run test:simple && npm run test:jest && npm run test:tape && npm run test:native && npm run test:esbuild && npm run test:pure && npm run test:fetch && npm run test:jsdom && npm run test:bundle",
|
|
88
90
|
"test:native": "EXODUS_TEST_IGNORE='{**/typescript/**,**/jest-repo/**/user.test.js}' ./bin/index.js --jest 'tests/**/*.test.{js,cjs,mjs}'",
|
|
89
91
|
"test:typescript": "node ./bin/index.js --jest --typescript tests/typescript.test.ts",
|
|
90
92
|
"test:jest": "node ./bin/index.js --jest --esbuild=ts,user.test.js,sum.test.js",
|
|
@@ -128,8 +130,7 @@
|
|
|
128
130
|
"optionalDependencies": {
|
|
129
131
|
"@chalker/queue": "^1.0.1",
|
|
130
132
|
"@exodus/replay": "^1.0.0-rc.9",
|
|
131
|
-
"@exodus/test-bundler": "1.0.0-rc.
|
|
132
|
-
"amaro": "^0.0.5",
|
|
133
|
+
"@exodus/test-bundler": "1.0.0-rc.4",
|
|
133
134
|
"c8": "^9.1.0",
|
|
134
135
|
"expect": "^29.7.0",
|
|
135
136
|
"fast-glob": "^3.2.11",
|
package/src/jest.config.js
CHANGED
|
@@ -81,6 +81,8 @@ export async function loadJestConfig(...args) {
|
|
|
81
81
|
if (config.globalTeardown) config.globalTeardown = req.resolve(config.globalTeardown) // eslint-disable-line @exodus/mutable/no-param-reassign-prop-only
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
const presetExtension = /\.([cm]?js|json)$/u
|
|
85
|
+
const suffixes = ['/jest-preset.json', '/jest-preset.js', '/jest-preset.cjs', '/jest-preset.mjs']
|
|
84
86
|
if (needPreset(rawConfig) || rawConfig.globalSetup || rawConfig.globalTeardown) {
|
|
85
87
|
rawConfig.preset = cleanFile(rawConfig.preset) // relative to root dir only at top level, presets shouldn't use <rootDir>
|
|
86
88
|
if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
@@ -93,15 +95,11 @@ export async function loadJestConfig(...args) {
|
|
|
93
95
|
let requireConfig = createRequire(resolve(rawConfig.rootDir, 'package.json'))
|
|
94
96
|
resolveGlobalSetup(rawConfig, requireConfig)
|
|
95
97
|
while (needPreset(rawConfig)) {
|
|
96
|
-
const suffixes = rawConfig.preset.startsWith('.')
|
|
97
|
-
? ['']
|
|
98
|
-
: ['/jest-preset.json', '/jest-preset.js', '/jest-preset.cjs', '/jest-preset.mjs']
|
|
99
|
-
|
|
100
98
|
let baseConfig
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
|
|
100
|
+
const attemptLoad = async (file) => {
|
|
103
101
|
try {
|
|
104
|
-
const resolved = requireConfig.resolve(
|
|
102
|
+
const resolved = requireConfig.resolve(file)
|
|
105
103
|
// FIXME: fix linter to allow this
|
|
106
104
|
// const meta = resolved.toLowerCase().endsWith('.json') ? { with: { type: 'json' } } : undefined
|
|
107
105
|
// const presetModule = await import(pathToFileURL(resolved), meta)
|
|
@@ -111,6 +109,17 @@ export async function loadJestConfig(...args) {
|
|
|
111
109
|
} catch {}
|
|
112
110
|
}
|
|
113
111
|
|
|
112
|
+
// Even if it is relative, it could be a path to module
|
|
113
|
+
for (const suffix of suffixes) {
|
|
114
|
+
if (!baseConfig) await attemptLoad(`${rawConfig.preset}${suffix}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If it's a path to a file
|
|
118
|
+
if (!baseConfig && rawConfig.preset[0] === '.' && presetExtension.test(rawConfig.preset)) {
|
|
119
|
+
const { statSync } = await import('node:fs')
|
|
120
|
+
if (statSync(rawConfig.preset).isFile()) await attemptLoad(rawConfig.preset)
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
assert(baseConfig, `Could not load preset: ${rawConfig.preset} `)
|
|
115
124
|
resolveGlobalSetup(baseConfig, requireConfig)
|
|
116
125
|
rawConfig = {
|
package/src/jest.mock.js
CHANGED
|
@@ -11,44 +11,55 @@ import {
|
|
|
11
11
|
import { jestfn } from './jest.fn.js'
|
|
12
12
|
import { loadExpect } from './expect.cjs'
|
|
13
13
|
import { loadPrettyFormat } from './pretty-format.cjs'
|
|
14
|
-
import { makeEsbuildMockable, insideEsbuild } from './dark.cjs'
|
|
14
|
+
import { makeEsbuildMockable, insideEsbuild, createCallerLocationHook } from './dark.cjs'
|
|
15
15
|
|
|
16
16
|
const mapMocks = new Map()
|
|
17
17
|
const mapActual = new Map()
|
|
18
18
|
const nodeMocks = new Map()
|
|
19
19
|
const overridenBuiltins = new Set()
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
impl(...args)
|
|
24
|
-
return this
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
21
|
+
// TODO: support correct relative locations in other engines too (and bundles)
|
|
22
|
+
const { getCallerLocation: getLoc } = createCallerLocationHook()
|
|
28
23
|
export const jestModuleMocks = {
|
|
29
|
-
mock
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
mock(name, mock) {
|
|
25
|
+
jestmock(name, mock, { override: true, loc: getLoc() })
|
|
26
|
+
return this
|
|
27
|
+
},
|
|
28
|
+
doMock(name, mock) {
|
|
29
|
+
jestmock(name, mock, { loc: getLoc() })
|
|
30
|
+
return this
|
|
31
|
+
},
|
|
32
|
+
setMock(name, mock) {
|
|
33
|
+
jestmock(name, () => mock, { loc: getLoc() }) // like doMock, does not hoist to top, tested
|
|
34
|
+
return this
|
|
35
|
+
},
|
|
36
|
+
unmock(name) {
|
|
37
|
+
unmock(name, { loc: getLoc() })
|
|
38
|
+
return this
|
|
39
|
+
},
|
|
40
|
+
createMockFromModule: (name) => mockClone(requireActual(name, { loc: getLoc() })),
|
|
41
|
+
requireMock: (name) => requireMock(name, { loc: getLoc() }),
|
|
42
|
+
requireActual: (name) => requireActual(name, { loc: getLoc() }),
|
|
37
43
|
resetModules,
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
jestModuleMocks.dontMock = jestModuleMocks.unmock
|
|
47
|
+
|
|
40
48
|
if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
41
49
|
globalThis.EXODUS_TEST_MOCK_BUILTINS = new Map()
|
|
42
50
|
Object.assign(jestModuleMocks, {
|
|
43
|
-
__mockBundle
|
|
51
|
+
__mockBundle(name, builtin, actual, mock) {
|
|
44
52
|
jestmock(name, mock, { actual, builtin, override: true })
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
return this
|
|
54
|
+
},
|
|
55
|
+
__doMockBundle(name, builtin, actual, mock) {
|
|
47
56
|
jestmock(name, mock, { actual, builtin })
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
return this
|
|
58
|
+
},
|
|
59
|
+
__setMockBundle(name, builtin, actual, mock) {
|
|
50
60
|
jestmock(name, () => mock, { actual, builtin })
|
|
51
|
-
|
|
61
|
+
return this
|
|
62
|
+
},
|
|
52
63
|
})
|
|
53
64
|
}
|
|
54
65
|
|
|
@@ -56,7 +67,7 @@ if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
|
56
67
|
const cjsSet = typeof __mocksCJSPossible === 'undefined' ? null : __mocksCJSPossible // eslint-disable-line no-undef
|
|
57
68
|
const esmSet = typeof __mocksESMPossible === 'undefined' ? null : __mocksESMPossible // eslint-disable-line no-undef
|
|
58
69
|
|
|
59
|
-
function resolveModule(name) {
|
|
70
|
+
function resolveModule(name, loc) {
|
|
60
71
|
if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
|
|
61
72
|
assert(name.startsWith('bundle:'), `Can't mock unresolved ${name} in bundle, use static syntax`)
|
|
62
73
|
assert(cjsSet && esmSet, 'Module mocking not installed correctly in bundle')
|
|
@@ -72,30 +83,33 @@ function resolveModule(name) {
|
|
|
72
83
|
return id
|
|
73
84
|
}
|
|
74
85
|
|
|
75
|
-
assert(requireIsRelative || /^[@a-zA-Z]/u.test(name), 'Mocking relative paths is not possible')
|
|
76
86
|
const unprefixed = name.replace(/^node:/, '')
|
|
77
87
|
if (builtinModules.includes(unprefixed)) return unprefixed
|
|
88
|
+
if (loc?.[2]) return require('node:module').createRequire(loc?.[2]).resolve(name)
|
|
89
|
+
assert(requireIsRelative || /^[@a-zA-Z]/u.test(name), 'Mocking relative paths is not possible')
|
|
78
90
|
return require.resolve(name)
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
function resolveImport(name) {
|
|
93
|
+
function resolveImport(name, loc) {
|
|
82
94
|
try {
|
|
83
|
-
const { fileURLToPath } = require('node:url')
|
|
84
|
-
|
|
95
|
+
const { fileURLToPath, pathToFileURL } = require('node:url')
|
|
96
|
+
let parent
|
|
97
|
+
if (loc?.[2]) parent = loc[2].startsWith('file:') ? loc[2] : pathToFileURL(loc[2])
|
|
98
|
+
return fileURLToPath(import.meta.resolve(name, parent))
|
|
85
99
|
} catch {
|
|
86
100
|
return null
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
function requireActual(name) {
|
|
91
|
-
const resolved = resolveModule(name)
|
|
104
|
+
function requireActual(name, { loc } = {}) {
|
|
105
|
+
const resolved = resolveModule(name, loc)
|
|
92
106
|
if (mapActual.has(resolved)) return mapActual.get(resolved)
|
|
93
107
|
if (!mapMocks.has(resolved)) return require(resolved)
|
|
94
108
|
throw new Error('Module can not been loaded')
|
|
95
109
|
}
|
|
96
110
|
|
|
97
|
-
function requireMock(name) {
|
|
98
|
-
const resolved = resolveModule(name)
|
|
111
|
+
function requireMock(name, { loc } = {}) {
|
|
112
|
+
const resolved = resolveModule(name, loc)
|
|
99
113
|
assert(mapMocks.has(resolved), 'Module is not mocked')
|
|
100
114
|
return mapMocks.get(resolved)
|
|
101
115
|
}
|
|
@@ -112,8 +126,8 @@ function resetModules() {
|
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
|
|
115
|
-
function unmock(name) {
|
|
116
|
-
const resolved = resolveModule(name)
|
|
129
|
+
function unmock(name, { loc } = {}) {
|
|
130
|
+
const resolved = resolveModule(name, loc)
|
|
117
131
|
assert(mapMocks.has(resolved), 'Module is not mocked')
|
|
118
132
|
if (mockModule) nodeMocks.get(resolved).restore()
|
|
119
133
|
delete require.cache[resolved]
|
|
@@ -281,7 +295,7 @@ if (process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle') {
|
|
|
281
295
|
}
|
|
282
296
|
}
|
|
283
297
|
|
|
284
|
-
function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
|
|
298
|
+
function jestmock(name, mocker, { override = false, actual, builtin, loc } = {}) {
|
|
285
299
|
// Loaded ESM: isn't mocked
|
|
286
300
|
// Loaded CJS: mocked via object overriding
|
|
287
301
|
// Loaded built-ins: mocked via object overriding where possible
|
|
@@ -294,7 +308,7 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
|
|
|
294
308
|
|
|
295
309
|
const mockFromMocks = mocker ? undefined : loadMocksDirMock?.(name)
|
|
296
310
|
|
|
297
|
-
const resolved = resolveModule(name)
|
|
311
|
+
const resolved = resolveModule(name, loc)
|
|
298
312
|
const isBuiltIn = builtinModules.includes(resolved)
|
|
299
313
|
if (!mocker && mockFromMocks && mapMocks.get(resolved) === mockFromMocks) return
|
|
300
314
|
assert(!mapMocks.has(resolved), 'Re-mocking the same module is not supported')
|
|
@@ -346,8 +360,8 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
|
|
|
346
360
|
return this
|
|
347
361
|
}
|
|
348
362
|
|
|
349
|
-
const
|
|
350
|
-
let likelyESM =
|
|
363
|
+
const topESM = isTopLevelESM()
|
|
364
|
+
let likelyESM = topESM && !insideEsbuild() && ![null, resolved].includes(resolveImport(name, loc))
|
|
351
365
|
let isOverridenBuiltinSynchedWithESM = false
|
|
352
366
|
const isNodeCache = (x) => x && x.id && x.path && x.filename && x.children && x.paths && x.loaded
|
|
353
367
|
if (isBuiltIn && !isNodeCache(require.cache[resolved])) {
|
|
@@ -386,7 +400,7 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
|
|
|
386
400
|
}
|
|
387
401
|
|
|
388
402
|
const mocksNodeVersionNote = 'mocks are available only on Node.js >=20.18 <21 || >=22.3'
|
|
389
|
-
if (likelyESM || (!isOverridenBuiltinSynchedWithESM &&
|
|
403
|
+
if (likelyESM || (!isOverridenBuiltinSynchedWithESM && topESM)) {
|
|
390
404
|
// Native module mocks is required if loading ESM or __from__ ESM
|
|
391
405
|
// No good way to check the locations that import the module, but we can check top-level file
|
|
392
406
|
// Built-in modules are fine though
|
|
@@ -395,17 +409,19 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
|
|
|
395
409
|
assert(mockModule, `Native non-overriding node:* ${mocksNodeVersionNote}`)
|
|
396
410
|
}
|
|
397
411
|
|
|
398
|
-
if (value[Symbol.toStringTag] === 'Module') value.__esModule = true
|
|
412
|
+
if (value?.[Symbol.toStringTag] === 'Module') value.__esModule = true
|
|
399
413
|
const obj = { defaultExport: value }
|
|
400
414
|
if (isBuiltIn && isObject(value)) obj.namedExports = value
|
|
401
415
|
if (insideEsbuild()) {
|
|
402
416
|
// esbuild handles unwrapping just default exports for us
|
|
403
417
|
assert(!likelyESM) // should not be reachable
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
418
|
+
if (isObject(value)) {
|
|
419
|
+
const { default: defaultExport, __esModule, ...namedExports } = value // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
420
|
+
// Don't override defaultExport, as that's processed with esbuild
|
|
421
|
+
// Add named exports though for further static named imports from that module
|
|
422
|
+
// type:module and esbuild can be combined e.g. when testing typescript packages
|
|
423
|
+
if (__esModule) obj.namedExports = namedExports
|
|
424
|
+
}
|
|
409
425
|
} else if (likelyESM && isObject(value) && value.__esModule === true) {
|
|
410
426
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
411
427
|
const { default: defaultExport, __esModule, ...namedExports } = value
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { loadJestConfig, installJestEnvironment } from './jest.config.js'
|
|
2
|
+
|
|
3
|
+
export async function setupJest() {
|
|
4
|
+
await loadJestConfig()
|
|
5
|
+
const { should, ...jestGlobals } = await import('./jest.js') // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
6
|
+
await installJestEnvironment(jestGlobals)
|
|
7
|
+
}
|