@exodus/test 1.0.0-rc.18 → 1.0.0-rc.2
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/README.md +28 -45
- package/bin/index.js +37 -177
- package/bin/preload.js +3 -0
- package/package.json +13 -46
- package/src/index.js +163 -0
- package/bin/babel.cjs +0 -8
- package/bin/jest.js +0 -5
- package/src/dark.cjs +0 -110
- package/src/jest.config.js +0 -149
- package/src/jest.environment.js +0 -41
- package/src/jest.fn.js +0 -158
- package/src/jest.js +0 -176
- package/src/jest.mock.js +0 -136
- package/src/jest.snapshot.js +0 -171
- package/src/jest.timers.js +0 -67
- package/src/tape.js +0 -162
- package/src/version.js +0 -16
package/README.md
CHANGED
|
@@ -1,44 +1,20 @@
|
|
|
1
1
|
# @exodus/test
|
|
2
2
|
|
|
3
|
-
A runner for `node:test`, `jest`, and `tape` test suites on top of `node:test`
|
|
4
|
-
|
|
5
3
|
Most likely it will just work on your simple jest tests as as drop-in replacement
|
|
6
4
|
|
|
7
|
-
Comes with typescript support, optional esm/cjs interop, and also loading babel transforms!
|
|
8
|
-
|
|
9
|
-
Use `--coverage` to generate coverage output
|
|
10
|
-
|
|
11
|
-
Default `NODE_ENV` value is "test", use `NODE_ENV=` to override (e.g. to empty)
|
|
12
|
-
|
|
13
5
|
## Library
|
|
14
6
|
|
|
15
|
-
### Using with `node:test` natively
|
|
16
|
-
|
|
17
|
-
You can just use pure [`node:test`](https://nodejs.org/api/test.html) in your tests,
|
|
18
|
-
this runner is fully compatible with that (and will set version-specific options for you)!
|
|
19
|
-
|
|
20
7
|
### Moving from jest
|
|
21
8
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
expect,
|
|
26
|
-
describe,
|
|
27
|
-
it,
|
|
28
|
-
beforeEach,
|
|
29
|
-
afterEach,
|
|
30
|
-
beforeAll,
|
|
31
|
-
afterAll,
|
|
32
|
-
} from '@exodus/test/jest'
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Or, run with [`--jest` option](#options) to register jest globals
|
|
9
|
+
`import { describe, it, assert, jest, expect } from '@exodus/test'`
|
|
10
|
+
|
|
11
|
+
Replace `.calls` with `.callsArguments`, as the former one now contains more detailed information
|
|
36
12
|
|
|
37
13
|
### Moving from tap/tape
|
|
38
14
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
15
|
+
`import { tap as test } from '@exodus/test'`
|
|
16
|
+
|
|
17
|
+
Not all features might be supported
|
|
42
18
|
|
|
43
19
|
### Running tests asynchronously
|
|
44
20
|
|
|
@@ -46,32 +22,39 @@ Add `{ concurrency: true }`, like this: `describe('my testsuite', { concurrency:
|
|
|
46
22
|
|
|
47
23
|
### List of exports
|
|
48
24
|
|
|
49
|
-
|
|
25
|
+
Adapters:
|
|
26
|
+
|
|
27
|
+
- `jest` -- jest mock adapter
|
|
28
|
+
- `tap` -- tap/tape adapter
|
|
29
|
+
- `mock`
|
|
50
30
|
|
|
51
|
-
|
|
31
|
+
Assertions:
|
|
32
|
+
|
|
33
|
+
- `assert` -- alias for `node:assert/strict`
|
|
34
|
+
- `expect` -- expect with additional features for function mocks
|
|
35
|
+
|
|
36
|
+
Suite:
|
|
37
|
+
|
|
38
|
+
- `describe`
|
|
39
|
+
- `test`
|
|
40
|
+
- `it` -- alias for `test`
|
|
41
|
+
- `beforeEach`
|
|
42
|
+
- `afterEach`
|
|
43
|
+
- `before` -- alias for `beforeAll`
|
|
44
|
+
- `after` -- alias for `afterAll`
|
|
52
45
|
|
|
53
46
|
## Binary
|
|
54
47
|
|
|
55
|
-
Just use `"test
|
|
48
|
+
Just use `"test: "exodus-test"`
|
|
56
49
|
|
|
57
50
|
### Options
|
|
58
51
|
|
|
59
|
-
- `--
|
|
60
|
-
|
|
61
|
-
- `--esbuild` -- use esbuild loader, also enables Typescript support
|
|
52
|
+
- `--global` -- register all test helpers as global variables
|
|
62
53
|
|
|
63
|
-
- `--
|
|
54
|
+
- `--typescript` -- enable typescript support
|
|
64
55
|
|
|
65
56
|
- `--coverage` -- enable coverage, prints coverage output (varies by coverage engine)
|
|
66
57
|
|
|
67
58
|
- `--coverage-engine c8` -- use c8 coverage engine (default), also generates `./coverage/` dirs
|
|
68
59
|
|
|
69
60
|
- `--coverage-engine node` -- use Node.js builtint coverage engine
|
|
70
|
-
|
|
71
|
-
- `--watch` -- operate in watch mode and re-run tests on file changes
|
|
72
|
-
|
|
73
|
-
- `--passWithNoTests` -- do not error when no test files were found
|
|
74
|
-
|
|
75
|
-
- `--write-snapshots` -- write snapshots instead of verifying them (has `--test-update-snapshots` alias)
|
|
76
|
-
|
|
77
|
-
- `--test-force-exit` -- force exit after tests are done (useful in integration tests where it could be unfeasible to resolve all open handles)
|
package/bin/index.js
CHANGED
|
@@ -5,26 +5,26 @@ import { fileURLToPath } from 'node:url'
|
|
|
5
5
|
import { basename, dirname, resolve } from 'node:path'
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
7
|
import assert from 'node:assert/strict'
|
|
8
|
-
import glob from 'fast-glob'
|
|
9
|
-
import { haveModuleMocks, haveSnapshots, haveForceExit } from '../src/version.js'
|
|
8
|
+
import glob from 'fast-glob' // Only for Node.js <22 support
|
|
10
9
|
|
|
11
10
|
const bindir = dirname(fileURLToPath(import.meta.url))
|
|
12
11
|
|
|
13
|
-
const
|
|
14
|
-
|
|
12
|
+
const DEFAULT_PATTERNS = ['**/*.{test,spec}.?(c|m)js', '**/*.{test,spec}.ts']
|
|
13
|
+
|
|
14
|
+
function versionCheck() {
|
|
15
|
+
const [major, minor, patch] = process.versions.node.split('.').map(Number)
|
|
16
|
+
assert((major === 18 && minor >= 13) || major >= 20, 'Node.js version too old!')
|
|
17
|
+
assert(major !== 21, 'Node.js version deprecated!')
|
|
18
|
+
|
|
19
|
+
return { major, minor, patch }
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
function parseOptions() {
|
|
17
23
|
const options = {
|
|
18
|
-
|
|
24
|
+
global: false,
|
|
19
25
|
typescript: false,
|
|
20
|
-
esbuild: false,
|
|
21
|
-
babel: false,
|
|
22
26
|
coverage: false,
|
|
23
27
|
coverageEngine: 'c8', // c8 or node
|
|
24
|
-
watch: false,
|
|
25
|
-
passWithNoTests: false,
|
|
26
|
-
writeSnapshots: false,
|
|
27
|
-
debug: { files: false },
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const args = [...process.argv]
|
|
@@ -40,20 +40,11 @@ function parseOptions() {
|
|
|
40
40
|
while (args[0]?.startsWith('--')) {
|
|
41
41
|
const option = args.shift()
|
|
42
42
|
switch (option) {
|
|
43
|
-
case '--global':
|
|
44
|
-
|
|
45
|
-
options.jest = true
|
|
43
|
+
case '--global':
|
|
44
|
+
options.global = true
|
|
46
45
|
break
|
|
47
46
|
case '--typescript':
|
|
48
|
-
console.warn('Option --typescript is going to be gone or changed. Use --esbuild instead')
|
|
49
47
|
options.typescript = true
|
|
50
|
-
options.esbuild = true
|
|
51
|
-
break
|
|
52
|
-
case '--esbuild':
|
|
53
|
-
options.esbuild = true
|
|
54
|
-
break
|
|
55
|
-
case '--babel':
|
|
56
|
-
options.babel = true
|
|
57
48
|
break
|
|
58
49
|
case '--coverage-engine':
|
|
59
50
|
options.coverageEngine = args.shift()
|
|
@@ -61,23 +52,6 @@ function parseOptions() {
|
|
|
61
52
|
case '--coverage':
|
|
62
53
|
options.coverage = true
|
|
63
54
|
break
|
|
64
|
-
case '--watch':
|
|
65
|
-
options.watch = true
|
|
66
|
-
break
|
|
67
|
-
case '--passWithNoTests':
|
|
68
|
-
options.passWithNoTests = true
|
|
69
|
-
break
|
|
70
|
-
case '--test-update-snapshots': // Node.js name for this, might get suggested in errors
|
|
71
|
-
case '--write-snapshots':
|
|
72
|
-
options.writeSnapshots = true
|
|
73
|
-
break
|
|
74
|
-
case '--test-force-exit':
|
|
75
|
-
case '--forceExit':
|
|
76
|
-
options.forceExit = true
|
|
77
|
-
break
|
|
78
|
-
case '--debug-files':
|
|
79
|
-
options.debug.files = true
|
|
80
|
-
break
|
|
81
55
|
default:
|
|
82
56
|
throw new Error(`Unknown option: ${option}`)
|
|
83
57
|
}
|
|
@@ -89,40 +63,20 @@ function parseOptions() {
|
|
|
89
63
|
)
|
|
90
64
|
|
|
91
65
|
const patterns = [...args]
|
|
66
|
+
if (patterns.length === 0) patterns.push(...DEFAULT_PATTERNS) // defaults
|
|
92
67
|
|
|
93
68
|
return { options, patterns }
|
|
94
69
|
}
|
|
95
70
|
|
|
71
|
+
const { major, minor } = versionCheck()
|
|
96
72
|
const { options, patterns } = parseOptions()
|
|
97
73
|
|
|
98
74
|
let program = 'node'
|
|
99
75
|
|
|
100
76
|
const require = createRequire(import.meta.url)
|
|
101
|
-
const
|
|
102
|
-
const resolveImport = import.meta.resolve && ((query) => fileURLToPath(import.meta.resolve(query)))
|
|
103
|
-
|
|
104
|
-
const c8 = resolveRequire('c8/bin/c8.js')
|
|
105
|
-
if (resolveImport) assert.equal(c8, resolveImport('c8/bin/c8.js'))
|
|
106
|
-
|
|
107
|
-
const args = ['--test', '--no-warnings=ExperimentalWarning']
|
|
108
|
-
|
|
109
|
-
if (haveModuleMocks) args.push('--experimental-test-module-mocks')
|
|
110
|
-
if (haveSnapshots) args.push('--experimental-test-snapshots')
|
|
111
|
-
|
|
112
|
-
if (options.writeSnapshots) {
|
|
113
|
-
assert(haveSnapshots, 'For snapshots, use Node.js >=22.3.0')
|
|
114
|
-
args.push('--test-update-snapshots')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (options.forceExit) {
|
|
118
|
-
assert(haveForceExit, 'For forceExit, use Node.js >= 20.14.0')
|
|
119
|
-
args.push('--test-force-exit')
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (options.watch) args.push('--watch')
|
|
123
|
-
|
|
124
|
-
args.push('--expose-internals') // this is unoptimal and hopefully temporary, see rationale in src/dark.cjs
|
|
77
|
+
const c8 = require.resolve('c8/bin/c8.js')
|
|
125
78
|
|
|
79
|
+
const args = ['--test', '--enable-source-maps']
|
|
126
80
|
if (options.coverage) {
|
|
127
81
|
if (options.coverageEngine === 'node') {
|
|
128
82
|
args.push('--experimental-test-coverage')
|
|
@@ -136,129 +90,35 @@ if (options.coverage) {
|
|
|
136
90
|
}
|
|
137
91
|
}
|
|
138
92
|
|
|
139
|
-
if (options.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (options.babel) {
|
|
145
|
-
assert(!options.esbuild, 'Options --babel and --esbuild are mutually exclusive')
|
|
146
|
-
args.push('-r', resolveRequire('./babel.cjs'))
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const ignore = ['**/node_modules']
|
|
150
|
-
let filter
|
|
151
|
-
if (process.env.EXODUS_TEST_IGNORE) {
|
|
152
|
-
// fast-glob treats negative ignore patterns exactly the same as positive, let's not cause a confusion
|
|
153
|
-
assert(!process.env.EXODUS_TEST_IGNORE.startsWith('!'), 'Ignore pattern should not be negative')
|
|
154
|
-
ignore.push(process.env.EXODUS_TEST_IGNORE)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Our loader should be last, as enabling module mocks confuses other loaders
|
|
158
|
-
if (options.jest) {
|
|
159
|
-
const { loadJestConfig } = await import('../src/jest.config.js')
|
|
160
|
-
const config = await loadJestConfig(process.cwd())
|
|
161
|
-
args.push('--import', resolve(bindir, 'jest.js'))
|
|
162
|
-
|
|
163
|
-
if (config.testFailureExitCode !== undefined) {
|
|
164
|
-
if (Number(config.testFailureExitCode) === 0) {
|
|
165
|
-
console.warn('Jest is configured to succeed with exit code 0 on test failures!')
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
process.on('exit', (code) => {
|
|
169
|
-
if (code !== 0) process.exitCode = config.testFailureExitCode
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (patterns.length > 0) {
|
|
174
|
-
// skip, we already have patterns via argv
|
|
175
|
-
} else if (config.testRegex) {
|
|
176
|
-
assert(typeof config.testRegex === 'string', `config.testRegex should be a string`)
|
|
177
|
-
assert(!config.testMatch, 'config.testRegex can not be used together with config.testMatch')
|
|
178
|
-
patterns.push('**/*')
|
|
179
|
-
} else if (config.testMatch) {
|
|
180
|
-
patterns.push(...(Array.isArray(config.testMatch) ? config.testMatch : [config.testMatch]))
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const testRegex = config.testRegex ? new RegExp(config.testRegex, 'u') : null
|
|
184
|
-
const ignoreRegexes = config.testPathIgnorePatterns.map((x) => new RegExp(x, 'u'))
|
|
185
|
-
if (testRegex || ignoreRegexes.length > 0) {
|
|
186
|
-
filter = (x) => {
|
|
187
|
-
const resolved = `<rootDir>/${x}` // don't actually include cwd, that should be irrelevant
|
|
188
|
-
if (testRegex && !testRegex.test(resolved)) return false
|
|
189
|
-
return !ignoreRegexes.some((r) => r.test(resolved))
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (patterns.length === 0) patterns.push(...DEFAULT_PATTERNS) // defaults
|
|
195
|
-
const globbed = await glob(patterns, { ignore })
|
|
196
|
-
const allfiles = filter ? globbed.filter(filter) : globbed
|
|
197
|
-
|
|
198
|
-
if (allfiles.length === 0) {
|
|
199
|
-
if (options.passWithNoTests) {
|
|
200
|
-
console.warn('No test files found, but passing due to --passWithNoTests')
|
|
201
|
-
process.exit(0)
|
|
93
|
+
if (options.global) {
|
|
94
|
+
if (major >= 20 || (major === 18 && minor >= 18)) {
|
|
95
|
+
args.push('--import', resolve(bindir, 'preload.js'))
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error('Option --global requires Node.js >= v18.18.0')
|
|
202
98
|
}
|
|
203
|
-
|
|
204
|
-
console.error('No test files found!')
|
|
205
|
-
process.exit(1)
|
|
206
99
|
}
|
|
207
100
|
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const stray = subfiles.filter((file) => !allSet.has(file))
|
|
214
|
-
if (stray.length > 0) {
|
|
215
|
-
console.error(`Selected tests should be a subset of all tests:\n ${stray.join('\n ')}`)
|
|
216
|
-
process.exit(1)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (subfiles.length === 0) {
|
|
220
|
-
console.error('No test files selected due to EXODUS_TEST_SELECT, passing')
|
|
221
|
-
process.exit(0)
|
|
101
|
+
if (options.typescript) {
|
|
102
|
+
if (major >= 22 || (major === 20 && minor >= 6) || (major === 18 && minor >= 18)) {
|
|
103
|
+
args.push('--import', '@swc-node/register/esm-register')
|
|
104
|
+
} else {
|
|
105
|
+
throw new Error('Option --typescript requires Node.js >=20.6.0 || 18 >=18.18.0')
|
|
222
106
|
}
|
|
223
107
|
}
|
|
224
108
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (al.length < 2) return -1
|
|
236
|
-
if (bl.length < 2) return 1
|
|
237
|
-
// Prefer example/ over example-something/
|
|
238
|
-
const [an, bn] = [al, bl].map((list) => list.join(String.fromCodePoint(0)))
|
|
239
|
-
if (an < bn) return -1
|
|
240
|
-
if (an > bn) return 1
|
|
109
|
+
if (major === 18 || major === 20) {
|
|
110
|
+
// We need to expand glob patterns for these
|
|
111
|
+
const ignore = ['node_modules']
|
|
112
|
+
const files = await glob(patterns, { ignore })
|
|
113
|
+
assert(files.length > 0, 'No tests found!')
|
|
114
|
+
args.push(...files)
|
|
115
|
+
} else if (major >= 22) {
|
|
116
|
+
// Yay we have native glob support
|
|
117
|
+
args.push(...patterns)
|
|
118
|
+
} else {
|
|
241
119
|
throw new Error('Unreachable')
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
if (options.debug.files) {
|
|
245
|
-
for (const f of files) console.log(f) // joining with \n can get truncated, too big
|
|
246
|
-
process.exit(1) // do not succeed!
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const tsTests = files.filter((file) => /\.[mc]?tsx?$/u.test(file))
|
|
250
|
-
if (tsTests.length > 0 && !options.esbuild) {
|
|
251
|
-
console.error(`Some tests require --esbuild flag:\n ${tsTests.join('\n ')}`)
|
|
252
|
-
process.exit(1)
|
|
253
|
-
} else if (!allfiles.some((file) => file.endsWith('.ts')) && options.typescript) {
|
|
254
|
-
console.warn(`Flag --typescript has been used, but there were no TypeScript tests found!`)
|
|
255
120
|
}
|
|
256
121
|
|
|
257
|
-
assert(files.length > 0) // otherwise we can run recursively
|
|
258
|
-
args.push(...files)
|
|
259
|
-
|
|
260
|
-
if (!Object.hasOwn(process.env, 'NODE_ENV')) process.env.NODE_ENV = 'test'
|
|
261
|
-
|
|
262
122
|
assert(program && ['node', c8].includes(program))
|
|
263
123
|
const node = spawn(program, args, { stdio: 'inherit' })
|
|
264
124
|
|
package/bin/preload.js
ADDED
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/test",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
|
-
"homepage": "https://github.com/ExodusMovement/test",
|
|
6
|
+
"homepage": "https://github.com/ExodusMovement/exodus-hydra/tree/master/libraries/test",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/ExodusMovement/exodus-hydra/issues?q=is%3Aissue+is%3Aopen+label%3Aexodus-test"
|
|
9
|
+
},
|
|
7
10
|
"repository": {
|
|
8
11
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/ExodusMovement/
|
|
10
|
-
},
|
|
11
|
-
"bugs": {
|
|
12
|
-
"url": "https://github.com/ExodusMovement/test/issues"
|
|
12
|
+
"url": "git+https://github.com/ExodusMovement/exodus-hydra.git"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"test",
|
|
@@ -25,53 +25,20 @@
|
|
|
25
25
|
"bin": {
|
|
26
26
|
"exodus-test": "bin/index.js"
|
|
27
27
|
},
|
|
28
|
-
"
|
|
29
|
-
"./jest": "./src/jest.js",
|
|
30
|
-
"./tape": "./src/tape.js"
|
|
31
|
-
},
|
|
32
|
-
"prettier": "@exodus/prettier",
|
|
28
|
+
"main": "src/index.js",
|
|
33
29
|
"files": [
|
|
34
|
-
"bin/
|
|
35
|
-
"bin/jest.js",
|
|
36
|
-
"src/dark.cjs",
|
|
37
|
-
"src/jest.js",
|
|
38
|
-
"src/jest.config.js",
|
|
39
|
-
"src/jest.environment.js",
|
|
40
|
-
"src/jest.fn.js",
|
|
41
|
-
"src/jest.mock.js",
|
|
42
|
-
"src/jest.snapshot.js",
|
|
43
|
-
"src/jest.timers.js",
|
|
44
|
-
"src/tape.js",
|
|
45
|
-
"src/version.js",
|
|
30
|
+
"bin/preload.js",
|
|
46
31
|
"!__tests__",
|
|
47
32
|
"CHANGELOG.md"
|
|
48
33
|
],
|
|
49
34
|
"scripts": {
|
|
50
|
-
"test": "
|
|
51
|
-
"
|
|
52
|
-
"coverage": "./bin/index.js --jest --esbuild --coverage",
|
|
53
|
-
"lint": "prettier --list-different . && eslint .",
|
|
54
|
-
"lint:fix": "prettier --write . && eslint --fix ."
|
|
35
|
+
"test": "exodus-test --global --typescript",
|
|
36
|
+
"lint": "run -T eslint ."
|
|
55
37
|
},
|
|
56
38
|
"dependencies": {
|
|
57
|
-
"@
|
|
39
|
+
"@swc-node/register": "^1.8.0",
|
|
58
40
|
"c8": "^9.1.0",
|
|
59
41
|
"expect": "^29.7.0",
|
|
60
|
-
"fast-glob": "^3.2.11"
|
|
61
|
-
|
|
62
|
-
"pretty-format": "^29.7.0",
|
|
63
|
-
"tsx": "^4.16.2"
|
|
64
|
-
},
|
|
65
|
-
"devDependencies": {
|
|
66
|
-
"@exodus/eslint-config": "^5.24.0",
|
|
67
|
-
"@exodus/prettier": "^1.0.0",
|
|
68
|
-
"@jest/globals": "^29.7.0",
|
|
69
|
-
"@types/jest-when": "^3.5.2",
|
|
70
|
-
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
|
71
|
-
"eslint": "^8.44.0",
|
|
72
|
-
"jest-matcher-utils": "^29.7.0",
|
|
73
|
-
"jest-serializer-ansi-escapes": "^3.0.0",
|
|
74
|
-
"jest-when": "^3.6.0"
|
|
75
|
-
},
|
|
76
|
-
"packageManager": "pnpm@9.4.0+sha1.9217c800d4ab947a7aee520242a7b70d64fc7638"
|
|
42
|
+
"fast-glob": "^3.2.11"
|
|
43
|
+
}
|
|
77
44
|
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { mock, describe, test, it } from 'node:test'
|
|
2
|
+
import { expect } from 'expect'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
import { format } from 'node:util'
|
|
5
|
+
|
|
6
|
+
const MockFunctionContext = mock.fn().mock.constructor
|
|
7
|
+
|
|
8
|
+
Object.defineProperties(MockFunctionContext.prototype, {
|
|
9
|
+
// this getter is called just .calls in jest, we document this difference
|
|
10
|
+
callsArguments: {
|
|
11
|
+
get() {
|
|
12
|
+
return this.calls.map((call) => call.arguments)
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
lastCall: {
|
|
16
|
+
get() {
|
|
17
|
+
return this.calls.at(-1)?.arguments
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
results: {
|
|
21
|
+
get() {
|
|
22
|
+
return this.calls.map((call) => ({ value: call.result }))
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const mockImplementationOrig = MockFunctionContext.prototype.mockImplementation
|
|
28
|
+
MockFunctionContext.prototype.mockImplementation = function (...args) {
|
|
29
|
+
mockImplementationOrig.call(this, ...args)
|
|
30
|
+
return this
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
MockFunctionContext.prototype.mockRestore = MockFunctionContext.prototype.restore
|
|
34
|
+
|
|
35
|
+
const makeEach = (impl) => (list) => (template, fn) => {
|
|
36
|
+
for (const args of list) {
|
|
37
|
+
let name = template
|
|
38
|
+
|
|
39
|
+
if (!args || typeof args === 'string' || typeof args === 'number') {
|
|
40
|
+
name = format(name, args)
|
|
41
|
+
} else {
|
|
42
|
+
for (const [key, value] of Object.entries(args)) {
|
|
43
|
+
name = name.replace(`$${key}`, value) // can collide but we don't care much yet
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (Array.isArray(args)) {
|
|
47
|
+
name = format(name, ...args)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl(name, () => (Array.isArray(args) ? fn(...args) : fn(args)))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe.each = makeEach(describe)
|
|
56
|
+
test.each = makeEach(test)
|
|
57
|
+
it.each = makeEach(it)
|
|
58
|
+
|
|
59
|
+
const [major, minor] = process.versions.node.split('.').map(Number)
|
|
60
|
+
|
|
61
|
+
const assertHaveTimers = () =>
|
|
62
|
+
assert(mock.timers, 'Timer mocking requires Node.js >=20.4.0 || 18 >=18.19.0')
|
|
63
|
+
|
|
64
|
+
let timersWarned = false
|
|
65
|
+
const warnOldTimers = () => {
|
|
66
|
+
if (timersWarned) return
|
|
67
|
+
timersWarned = true
|
|
68
|
+
const ok = major >= 22 || (major === 20 && minor >= 11)
|
|
69
|
+
if (!ok) console.warn('Warning: timer mocks are known to be glitchy before Node.js >=20.11.0')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const jest = {
|
|
73
|
+
fn: (...args) => mock.fn(...args),
|
|
74
|
+
spyOn: (obj, name) => {
|
|
75
|
+
assert(Object.hasOwn(obj, name))
|
|
76
|
+
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
77
|
+
obj[name] = mock.fn(obj[name])
|
|
78
|
+
return obj[name].mock
|
|
79
|
+
},
|
|
80
|
+
useFakeTimers: () => {
|
|
81
|
+
assertHaveTimers()
|
|
82
|
+
warnOldTimers()
|
|
83
|
+
try {
|
|
84
|
+
mock.timers.enable()
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// We allow calling this multiple times and swallow the "MockTimers is already enabled!" error
|
|
87
|
+
if (e.code !== 'ERR_INVALID_STATE') throw e
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
runAllTimers: () => {
|
|
91
|
+
assertHaveTimers()
|
|
92
|
+
warnOldTimers()
|
|
93
|
+
mock.timers.tick(100_000_000_000) // > 3 years
|
|
94
|
+
},
|
|
95
|
+
runOnlyPendingTimers: () => {
|
|
96
|
+
const noInfiniteLoopBug = major >= 22 || (major === 20 && minor >= 11)
|
|
97
|
+
assert(noInfiniteLoopBug, 'runOnlyPendingTimers requires Node.js >=20.11.0')
|
|
98
|
+
mock.timers.runAll()
|
|
99
|
+
},
|
|
100
|
+
advanceTimersByTime: (time) => {
|
|
101
|
+
assertHaveTimers()
|
|
102
|
+
warnOldTimers()
|
|
103
|
+
mock.timers.tick(time)
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
expect.extend({
|
|
108
|
+
toHaveBeenCalled: (fn) => {
|
|
109
|
+
assert.equal(fn?.mock?.constructor, MockFunctionContext)
|
|
110
|
+
return { pass: fn.mock.callCount() > 0 }
|
|
111
|
+
},
|
|
112
|
+
toHaveBeenCalledTimes: (fn, count) => {
|
|
113
|
+
assert.equal(fn?.mock?.constructor, MockFunctionContext)
|
|
114
|
+
return { pass: fn.mock.callCount() === count }
|
|
115
|
+
},
|
|
116
|
+
toHaveBeenCalledWith: (fn, ...expected) => {
|
|
117
|
+
assert.equal(fn?.mock?.constructor, MockFunctionContext)
|
|
118
|
+
for (const call of fn.mock.calls) {
|
|
119
|
+
try {
|
|
120
|
+
expect(call.arguments).toEqual(expected)
|
|
121
|
+
return { pass: true }
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { pass: false }
|
|
126
|
+
},
|
|
127
|
+
toHaveBeenLastCalledWith: (fn, ...expected) => {
|
|
128
|
+
assert.equal(fn?.mock?.constructor, MockFunctionContext)
|
|
129
|
+
try {
|
|
130
|
+
expect(fn.mock.calls.at(-1).arguments).toEqual(expected)
|
|
131
|
+
return { pass: true }
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return { pass: false, message: () => e.message }
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
function tap(name, fn) {
|
|
139
|
+
test(name, () =>
|
|
140
|
+
fn({
|
|
141
|
+
...assert,
|
|
142
|
+
pass: (name) => it(true, name),
|
|
143
|
+
end: () => {},
|
|
144
|
+
})
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { tap, jest }
|
|
149
|
+
export { expect } from 'expect'
|
|
150
|
+
export {
|
|
151
|
+
mock,
|
|
152
|
+
beforeEach,
|
|
153
|
+
before,
|
|
154
|
+
afterEach,
|
|
155
|
+
after,
|
|
156
|
+
before as beforeAll,
|
|
157
|
+
after as afterAll,
|
|
158
|
+
describe,
|
|
159
|
+
test,
|
|
160
|
+
it,
|
|
161
|
+
} from 'node:test'
|
|
162
|
+
|
|
163
|
+
export { default as assert } from 'node:assert/strict'
|
package/bin/babel.cjs
DELETED