@exodus/test 1.0.0-rc.17 → 1.0.0-rc.19
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 +7 -29
- package/package.json +3 -1
- package/src/jest.fn.js +0 -1
- package/src/jest.js +15 -4
- package/src/jest.mock.js +8 -6
- package/src/jest.snapshot.js +19 -7
- package/src/jest.timers.js +4 -7
- package/src/tape.js +1 -0
- package/src/version.js +16 -0
package/bin/index.js
CHANGED
|
@@ -6,20 +6,13 @@ import { basename, dirname, resolve } from 'node:path'
|
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
7
|
import assert from 'node:assert/strict'
|
|
8
8
|
import glob from 'fast-glob'
|
|
9
|
+
import { haveModuleMocks, haveSnapshots, haveForceExit } from '../src/version.js'
|
|
9
10
|
|
|
10
11
|
const bindir = dirname(fileURLToPath(import.meta.url))
|
|
11
12
|
|
|
12
|
-
const EXTS = `.?([cm])[jt]s?(x)` // we
|
|
13
|
+
const EXTS = `.?([cm])[jt]s?(x)` // we differ from jest, allowing [cm] before everything
|
|
13
14
|
const DEFAULT_PATTERNS = [`**/__tests__/**/*${EXTS}`, `**/?(*.)+(spec|test)${EXTS}`]
|
|
14
15
|
|
|
15
|
-
function versionCheck() {
|
|
16
|
-
const [major, minor, patch] = process.versions.node.split('.').map(Number)
|
|
17
|
-
assert((major === 18 && minor >= 13) || major >= 20, 'Node.js version too old!')
|
|
18
|
-
assert(major !== 21, 'Node.js version deprecated!')
|
|
19
|
-
|
|
20
|
-
return { major, minor, patch }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
function parseOptions() {
|
|
24
17
|
const options = {
|
|
25
18
|
jest: false,
|
|
@@ -100,7 +93,6 @@ function parseOptions() {
|
|
|
100
93
|
return { options, patterns }
|
|
101
94
|
}
|
|
102
95
|
|
|
103
|
-
const { major, minor } = versionCheck()
|
|
104
96
|
const { options, patterns } = parseOptions()
|
|
105
97
|
|
|
106
98
|
let program = 'node'
|
|
@@ -114,10 +106,7 @@ if (resolveImport) assert.equal(c8, resolveImport('c8/bin/c8.js'))
|
|
|
114
106
|
|
|
115
107
|
const args = ['--test', '--no-warnings=ExperimentalWarning']
|
|
116
108
|
|
|
117
|
-
const haveModuleMocks = major > 22 || (major === 22 && minor >= 3)
|
|
118
109
|
if (haveModuleMocks) args.push('--experimental-test-module-mocks')
|
|
119
|
-
|
|
120
|
-
const haveSnapshots = major > 22 || (major === 22 && minor >= 3)
|
|
121
110
|
if (haveSnapshots) args.push('--experimental-test-snapshots')
|
|
122
111
|
|
|
123
112
|
if (options.writeSnapshots) {
|
|
@@ -126,14 +115,11 @@ if (options.writeSnapshots) {
|
|
|
126
115
|
}
|
|
127
116
|
|
|
128
117
|
if (options.forceExit) {
|
|
129
|
-
assert(
|
|
118
|
+
assert(haveForceExit, 'For forceExit, use Node.js >= 20.14.0')
|
|
130
119
|
args.push('--test-force-exit')
|
|
131
120
|
}
|
|
132
121
|
|
|
133
|
-
if (options.watch)
|
|
134
|
-
assert((major === 18 && minor > 13) || major >= 20, 'For watch mode, use Node.js >= 18.13.0')
|
|
135
|
-
args.push('--watch')
|
|
136
|
-
}
|
|
122
|
+
if (options.watch) args.push('--watch')
|
|
137
123
|
|
|
138
124
|
args.push('--expose-internals') // this is unoptimal and hopefully temporary, see rationale in src/dark.cjs
|
|
139
125
|
|
|
@@ -151,12 +137,8 @@ if (options.coverage) {
|
|
|
151
137
|
}
|
|
152
138
|
|
|
153
139
|
if (options.esbuild) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
args.push('--import', resolveImport('tsx'))
|
|
157
|
-
} else {
|
|
158
|
-
args.push('-r', resolveRequire('tsx/cjs'))
|
|
159
|
-
}
|
|
140
|
+
assert(resolveImport)
|
|
141
|
+
args.push('--import', resolveImport('tsx'))
|
|
160
142
|
}
|
|
161
143
|
|
|
162
144
|
if (options.babel) {
|
|
@@ -176,11 +158,7 @@ if (process.env.EXODUS_TEST_IGNORE) {
|
|
|
176
158
|
if (options.jest) {
|
|
177
159
|
const { loadJestConfig } = await import('../src/jest.config.js')
|
|
178
160
|
const config = await loadJestConfig(process.cwd())
|
|
179
|
-
|
|
180
|
-
args.push('--import', resolve(bindir, 'jest.js'))
|
|
181
|
-
} else {
|
|
182
|
-
throw new Error('Option --jest requires Node.js >= v18.18.0')
|
|
183
|
-
}
|
|
161
|
+
args.push('--import', resolve(bindir, 'jest.js'))
|
|
184
162
|
|
|
185
163
|
if (config.testFailureExitCode !== undefined) {
|
|
186
164
|
if (Number(config.testFailureExitCode) === 0) {
|
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.19",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"src/jest.snapshot.js",
|
|
43
43
|
"src/jest.timers.js",
|
|
44
44
|
"src/tape.js",
|
|
45
|
+
"src/version.js",
|
|
45
46
|
"!__tests__",
|
|
46
47
|
"CHANGELOG.md"
|
|
47
48
|
],
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"@types/jest-when": "^3.5.2",
|
|
69
70
|
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
|
70
71
|
"eslint": "^8.44.0",
|
|
72
|
+
"jest": "^29.7.0",
|
|
71
73
|
"jest-matcher-utils": "^29.7.0",
|
|
72
74
|
"jest-serializer-ansi-escapes": "^3.0.0",
|
|
73
75
|
"jest-when": "^3.6.0"
|
package/src/jest.fn.js
CHANGED
package/src/jest.js
CHANGED
|
@@ -7,8 +7,10 @@ import { jestmock, requireActual, requireMock, resetModules } from './jest.mock.
|
|
|
7
7
|
import * as jestTimers from './jest.timers.js'
|
|
8
8
|
import './jest.snapshot.js'
|
|
9
9
|
import { createCallerLocationHook } from './dark.cjs'
|
|
10
|
+
import './version.js'
|
|
10
11
|
import { expect } from 'expect'
|
|
11
12
|
import matchers from 'jest-extended'
|
|
13
|
+
import { format as prettyFormat } from 'pretty-format'
|
|
12
14
|
|
|
13
15
|
const { getCallerLocation, installLocationInNextTest } = createCallerLocationHook()
|
|
14
16
|
|
|
@@ -46,8 +48,10 @@ const makeEach =
|
|
|
46
48
|
// Hack for common testing with simple arrow functions, until we can disable esbuild minification
|
|
47
49
|
const formatArg = (x) => (x && x instanceof Function && `${x}` === '()=>{}' ? '() => {}' : x)
|
|
48
50
|
// better than nothing
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
+
const printed = (x) =>
|
|
52
|
+
x && [null, Array.prototype, Object.prototype].includes(Object.getPrototypeOf(x))
|
|
53
|
+
? prettyFormat(x, { min: true })
|
|
54
|
+
: `${x}`
|
|
51
55
|
|
|
52
56
|
const args = parseArgs(list, rest)
|
|
53
57
|
const wrapped = args.every((x) => Array.isArray(x))
|
|
@@ -81,9 +85,11 @@ const makeEach =
|
|
|
81
85
|
const forceExit = process.execArgv.map((x) => x.replaceAll('_', '-')).includes('--test-force-exit')
|
|
82
86
|
|
|
83
87
|
const inConcurrent = []
|
|
88
|
+
const inDescribe = []
|
|
84
89
|
const concurrent = []
|
|
85
90
|
const describe = (...args) => {
|
|
86
91
|
const fn = args.pop()
|
|
92
|
+
inDescribe.push(fn)
|
|
87
93
|
const optionsConcurrent = args?.at(-1)?.concurrency > 1
|
|
88
94
|
if (optionsConcurrent) inConcurrent.push(fn)
|
|
89
95
|
const result = nodeDescribe(...args, async () => {
|
|
@@ -105,6 +111,7 @@ const describe = (...args) => {
|
|
|
105
111
|
return res
|
|
106
112
|
})
|
|
107
113
|
if (optionsConcurrent) inConcurrent.pop()
|
|
114
|
+
inDescribe.pop()
|
|
108
115
|
return result
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -129,8 +136,12 @@ const test = (...args) => testRaw(getCallerLocation(), ...args)
|
|
|
129
136
|
|
|
130
137
|
describe.each = makeEach(describe)
|
|
131
138
|
test.each = makeEach(test)
|
|
132
|
-
test.concurrent = (...args) =>
|
|
133
|
-
|
|
139
|
+
test.concurrent = (...args) => {
|
|
140
|
+
assert(inDescribe.length > 0, 'test.concurrent is supported only within a describe block')
|
|
141
|
+
if (inConcurrent.length > 0) return test(...args)
|
|
142
|
+
concurrent.push([args, getCallerLocation()])
|
|
143
|
+
}
|
|
144
|
+
|
|
134
145
|
test.concurrent.each = makeEach(test.concurrent)
|
|
135
146
|
describe.skip = (...args) => nodeDescribe.skip(...args)
|
|
136
147
|
test.skip = (...args) => nodeTest.skip(...args)
|
package/src/jest.mock.js
CHANGED
|
@@ -16,6 +16,8 @@ export const relativeRequire = require
|
|
|
16
16
|
|
|
17
17
|
export function resolveModule(name) {
|
|
18
18
|
assert(baseUrl || /^[@a-zA-Z]/u.test(name), 'Mocking relative paths is not possible')
|
|
19
|
+
const unprefixed = name.replace(/^node:/, '')
|
|
20
|
+
if (builtinModules.includes(unprefixed)) return unprefixed
|
|
19
21
|
return require.resolve(name)
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -70,7 +72,7 @@ function mockClone(root) {
|
|
|
70
72
|
if (obj instanceof RegExp) return [new RegExp(), false] // this is what jest does apparently
|
|
71
73
|
if (seen.has(obj)) return [seen.get(obj), !simple.has(obj)]
|
|
72
74
|
if (obj instanceof Function) {
|
|
73
|
-
seen.set(obj, jestfn(
|
|
75
|
+
seen.set(obj, jestfn())
|
|
74
76
|
return [seen.get(obj), true]
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -96,8 +98,6 @@ function mockClone(root) {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
export function jestmock(name, mocker) {
|
|
99
|
-
assert(mock.module, 'ESM module mocks are available only on Node.js >=22.3')
|
|
100
|
-
|
|
101
101
|
// Loaded ESM: isn't mocked
|
|
102
102
|
// Loaded CJS: mocked via object overriding
|
|
103
103
|
// Loaded built-ins: mocked via object overriding where possible
|
|
@@ -119,18 +119,20 @@ export function jestmock(name, mocker) {
|
|
|
119
119
|
const value = mocker ? { ...mocker() } : mockClone(mapActual.get(resolved))
|
|
120
120
|
mapMocks.set(resolved, value)
|
|
121
121
|
|
|
122
|
-
// fall through when e.g. this module doesn't exist or is ESM
|
|
123
122
|
if (Object.hasOwn(require.cache, resolved)) {
|
|
124
123
|
assert.equal(mapActual.get(resolved), require.cache[resolved].exports)
|
|
125
124
|
// If we did't have this prior but have now, it means we just loaded it and there are no leaked instances
|
|
126
125
|
if (havePrior) override(resolved)
|
|
127
126
|
require.cache[resolved].exports = value
|
|
128
|
-
} else if (builtinModules.includes(resolved
|
|
127
|
+
} else if (builtinModules.includes(resolved)) {
|
|
129
128
|
override(resolved, true) // Override builtin modules
|
|
130
129
|
syncBuiltinESMExports()
|
|
130
|
+
} else {
|
|
131
|
+
// The module doesn't exist or is ESM
|
|
132
|
+
assert(mock.module, 'ESM module mocks are available only on Node.js >=22.3')
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
mock.module(name, { defaultExport: value.default, namedExports: value })
|
|
135
|
+
mock.module?.(name, { defaultExport: value.default ?? value, namedExports: value })
|
|
134
136
|
|
|
135
137
|
return this
|
|
136
138
|
}
|
package/src/jest.snapshot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { beforeEach } from 'node:test'
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import { expect } from 'expect'
|
|
4
|
-
import { format } from 'pretty-format'
|
|
4
|
+
import { format, plugins as builtinPlugins } from 'pretty-format'
|
|
5
5
|
import assert from 'node:assert/strict'
|
|
6
6
|
import { basename, dirname, join, normalize } from 'node:path'
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
@@ -10,7 +10,7 @@ import { relativeRequire } from './jest.mock.js'
|
|
|
10
10
|
import { getTestNamePath } from './dark.cjs'
|
|
11
11
|
|
|
12
12
|
const { snapshotFormat, snapshotSerializers } = jestConfig()
|
|
13
|
-
const plugins =
|
|
13
|
+
const plugins = Object.values(builtinPlugins)
|
|
14
14
|
const serialize = (val) => format(val, { ...snapshotFormat, plugins }).replaceAll(/\r\n|\r/gu, '\n')
|
|
15
15
|
const resolveSnapshot = (f) => join(dirname(f), '__snapshots__', `${basename(f)}.snap`)
|
|
16
16
|
|
|
@@ -106,7 +106,16 @@ const snapInline = (obj, inline) => {
|
|
|
106
106
|
getAssert().strictEqual(serialize(obj).trim(), inline.trim())
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const snapOnDisk = (
|
|
109
|
+
const snapOnDisk = (orig, matcher) => {
|
|
110
|
+
if (matcher) {
|
|
111
|
+
expect(orig).toMatchObject(matcher)
|
|
112
|
+
// If we passed, make appear that the above call never happened
|
|
113
|
+
const state = expect.getState()
|
|
114
|
+
state.assertionCalls--
|
|
115
|
+
state.numPassingAsserts--
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const obj = matcher ? { ...orig, ...matcher } : orig
|
|
110
119
|
const escape = (str) => str.replaceAll(/([\\`])/gu, '\\$1')
|
|
111
120
|
|
|
112
121
|
if (!maybeSetupJestSnapshots()) {
|
|
@@ -142,16 +151,19 @@ const snapOnDisk = (obj) => {
|
|
|
142
151
|
try {
|
|
143
152
|
wrapContextName(() => getAssert().snapshot(obj))
|
|
144
153
|
} catch (e) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
if (typeof e.expected === 'string') {
|
|
155
|
+
const escaped = escape(e.expected)
|
|
156
|
+
const final = escaped.includes('\n') ? escaped : `\n${escaped}\n`
|
|
157
|
+
if (final === e.actual) return
|
|
158
|
+
}
|
|
159
|
+
|
|
148
160
|
throw e
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
expect.extend({
|
|
153
165
|
toMatchInlineSnapshot: (obj, i) => wrap(() => snapInline(obj, i)),
|
|
154
|
-
toMatchSnapshot: (obj) => wrap(() => snapOnDisk(obj)),
|
|
166
|
+
toMatchSnapshot: (obj, matcher) => wrap(() => snapOnDisk(obj, matcher)),
|
|
155
167
|
toThrowErrorMatchingInlineSnapshot: (...a) => wrap(() => throws(a, (m) => snapInline(m, a[1]))),
|
|
156
168
|
toThrowErrorMatchingSnapshot: (...a) => wrap(() => throws(a, (m) => snapOnDisk(m))),
|
|
157
169
|
})
|
package/src/jest.timers.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { mock } from 'node:test'
|
|
3
3
|
import { jestConfig } from './jest.config.js'
|
|
4
|
-
|
|
5
|
-
const [major, minor] = process.versions.node.split('.').map(Number)
|
|
4
|
+
import { haveValidTimers, haveNoTimerInfiniteLoopBug } from './version.js'
|
|
6
5
|
|
|
7
6
|
const assertHaveTimers = () =>
|
|
8
7
|
assert(mock.timers, 'Timer mocking requires Node.js >=20.4.0 || 18 >=18.19.0')
|
|
9
8
|
|
|
10
9
|
let timersWarned = false
|
|
11
10
|
const warnOldTimers = () => {
|
|
12
|
-
if (timersWarned) return
|
|
11
|
+
if (haveValidTimers || timersWarned) return
|
|
13
12
|
timersWarned = true
|
|
14
|
-
|
|
15
|
-
if (!ok) console.warn('Warning: timer mocks are known to be glitchy before Node.js >=20.11.0')
|
|
13
|
+
console.warn('Warning: timer mocks are known to be glitchy before Node.js >=20.11.0')
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
export function useRealTimers() {
|
|
@@ -47,8 +45,7 @@ export function runAllTimers() {
|
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
export function runOnlyPendingTimers() {
|
|
50
|
-
|
|
51
|
-
assert(noInfiniteLoopBug, 'runOnlyPendingTimers requires Node.js >=20.11.0')
|
|
48
|
+
assert(haveNoTimerInfiniteLoopBug, 'runOnlyPendingTimers requires Node.js >=20.11.0')
|
|
52
49
|
mock.timers.runAll()
|
|
53
50
|
return this
|
|
54
51
|
}
|
package/src/tape.js
CHANGED
|
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import assertLoose from 'node:assert'
|
|
3
3
|
import { test } from 'node:test'
|
|
4
4
|
import { createCallerLocationHook } from './dark.cjs'
|
|
5
|
+
import './version.js'
|
|
5
6
|
|
|
6
7
|
const knownOptions = new Set(['skip', 'todo', 'concurrency', 'timeout'])
|
|
7
8
|
|
package/src/version.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
|
|
3
|
+
const [major, minor, patch] = process.versions.node.split('.').map(Number)
|
|
4
|
+
assert(major !== 21, 'Node.js 21.x is deprecated!') // reached EOL, no reason to even test
|
|
5
|
+
// older versions are glitchy with before/after on top-level, which is a deal-breaker
|
|
6
|
+
const ok = (major === 18 && minor >= 19) || (major === 20 && minor >= 7) || major >= 22
|
|
7
|
+
assert(ok, 'Node.js version too old or glitchy with node:test, use ^18.19.0 || ^20.7.0 || >=22.0.0')
|
|
8
|
+
assert(major !== 22 || minor !== 3, 'Refusing to run on Node.js 22.3.0 specifically, do not use it') // safe-guard
|
|
9
|
+
|
|
10
|
+
export { major, minor, patch }
|
|
11
|
+
|
|
12
|
+
export const haveModuleMocks = (major === 22 && minor >= 3) || major > 22
|
|
13
|
+
export const haveSnapshots = (major === 22 && minor >= 3) || major > 22
|
|
14
|
+
export const haveForceExit = (major === 20 && minor > 13) || major >= 22
|
|
15
|
+
export const haveValidTimers = (major === 20 && minor >= 11) || major >= 22 // older glitch in various ways / stop executing
|
|
16
|
+
export const haveNoTimerInfiniteLoopBug = (major === 20 && minor >= 11) || major >= 22 // mock.timers.runAll() can get into infinite recursion
|