@exodus/test 1.0.0-rc.5 → 1.0.0-rc.7
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 +6 -0
- package/bin/index.js +21 -1
- package/bin/jest.js +3 -1
- package/package.json +3 -1
- package/src/jest.fn.js +24 -19
- package/src/jest.js +34 -18
- package/src/jest.mock.js +22 -11
- package/src/jest.snapshot.js +21 -24
- package/src/jest.timers.js +15 -13
- package/src/tape.js +45 -15
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Comes with typescript support, optional esm/cjs interop, and also loading babel
|
|
|
8
8
|
|
|
9
9
|
Use `--coverage` to generate coverage output
|
|
10
10
|
|
|
11
|
+
Default `NODE_ENV` value is "test", use `NODE_ENV=` to override (e.g. to empty)
|
|
12
|
+
|
|
11
13
|
## Library
|
|
12
14
|
|
|
13
15
|
### Using with `node:test` natively
|
|
@@ -68,6 +70,10 @@ Just use `"test": "exodus-test"`
|
|
|
68
70
|
|
|
69
71
|
- `--coverage-engine node` -- use Node.js builtint coverage engine
|
|
70
72
|
|
|
73
|
+
- `--watch` -- operate in watch mode and re-run tests on file changes
|
|
74
|
+
|
|
71
75
|
- `--passWithNoTests` -- do not error when no test files were found
|
|
72
76
|
|
|
73
77
|
- `--write-snapshots` -- write snapshots instead of verifying them (has `--test-update-snapshots` alias)
|
|
78
|
+
|
|
79
|
+
- `--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
|
@@ -26,9 +26,10 @@ function parseOptions() {
|
|
|
26
26
|
esbuild: false,
|
|
27
27
|
babel: false,
|
|
28
28
|
coverage: false,
|
|
29
|
+
coverageEngine: 'c8', // c8 or node
|
|
30
|
+
watch: false,
|
|
29
31
|
passWithNoTests: false,
|
|
30
32
|
writeSnapshots: false,
|
|
31
|
-
coverageEngine: 'c8', // c8 or node
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const args = [...process.argv]
|
|
@@ -63,6 +64,9 @@ function parseOptions() {
|
|
|
63
64
|
case '--coverage':
|
|
64
65
|
options.coverage = true
|
|
65
66
|
break
|
|
67
|
+
case '--watch':
|
|
68
|
+
options.watch = true
|
|
69
|
+
break
|
|
66
70
|
case '--passWithNoTests':
|
|
67
71
|
options.passWithNoTests = true
|
|
68
72
|
break
|
|
@@ -70,6 +74,10 @@ function parseOptions() {
|
|
|
70
74
|
case '--write-snapshots':
|
|
71
75
|
options.writeSnapshots = true
|
|
72
76
|
break
|
|
77
|
+
case '--test-force-exit':
|
|
78
|
+
case '--forceExit':
|
|
79
|
+
options.forceExit = true
|
|
80
|
+
break
|
|
73
81
|
default:
|
|
74
82
|
throw new Error(`Unknown option: ${option}`)
|
|
75
83
|
}
|
|
@@ -111,6 +119,16 @@ if (options.writeSnapshots) {
|
|
|
111
119
|
args.push('--test-update-snapshots')
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
if (options.forceExit) {
|
|
123
|
+
assert((major === 20 && minor > 13) || major >= 22, 'For forceExit, use Node.js >= 20.14.0')
|
|
124
|
+
args.push('--test-force-exit')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (options.watch) {
|
|
128
|
+
assert((major === 18 && minor > 13) || major >= 20, 'For watch mode, use Node.js >= 18.13.0')
|
|
129
|
+
args.push('--watch')
|
|
130
|
+
}
|
|
131
|
+
|
|
114
132
|
if (options.coverage) {
|
|
115
133
|
if (options.coverageEngine === 'node') {
|
|
116
134
|
args.push('--experimental-test-coverage')
|
|
@@ -172,6 +190,8 @@ if (tsTests.length > 0 && !options.typescript) {
|
|
|
172
190
|
assert(files.length > 0) // otherwise we can run recursively
|
|
173
191
|
args.push(...files)
|
|
174
192
|
|
|
193
|
+
if (!Object.hasOwn(process.env, 'NODE_ENV')) process.env.NODE_ENV = 'test'
|
|
194
|
+
|
|
175
195
|
assert(program && ['node', c8].includes(program))
|
|
176
196
|
const node = spawn(program, args, { stdio: 'inherit' })
|
|
177
197
|
|
package/bin/jest.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as globals from '../src/jest.js'
|
|
2
|
+
import { resolveModule } from '../src/jest.mock.js'
|
|
2
3
|
import { mock } from 'node:test'
|
|
3
4
|
|
|
4
5
|
Object.assign(globalThis, globals)
|
|
5
6
|
|
|
6
7
|
try {
|
|
7
|
-
|
|
8
|
+
const resolved = resolveModule('@jest/globals')
|
|
9
|
+
if (mock.module) mock.module(resolved, { defaultExport: globals, namedExports: globals })
|
|
8
10
|
} catch {}
|
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.7",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
46
|
"test": "./bin/index.js --jest --typescript",
|
|
47
|
+
"test:tape": "./bin/index.js --esbuild '__test__/tape/test/*.js' __test__/tape.test.js",
|
|
47
48
|
"coverage": "./bin/index.js --jest --typescript --coverage",
|
|
48
49
|
"lint": "prettier --list-different . && eslint .",
|
|
49
50
|
"lint:fix": "prettier --write . && eslint --fix ."
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@exodus/eslint-config": "^5.24.0",
|
|
61
62
|
"@exodus/prettier": "^1.0.0",
|
|
63
|
+
"@jest/globals": "^29.7.0",
|
|
62
64
|
"@types/jest-when": "^3.5.2",
|
|
63
65
|
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
|
64
66
|
"eslint": "^8.44.0",
|
package/src/jest.fn.js
CHANGED
|
@@ -3,15 +3,17 @@ import assert from 'node:assert/strict'
|
|
|
3
3
|
|
|
4
4
|
const registry = new Set()
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const applyAllWrap = (method) =>
|
|
7
|
+
function () {
|
|
8
|
+
assert(['mockClear', 'mockReset', 'mockRestore'].includes(method))
|
|
9
|
+
for (const obj of registry) obj[method]()
|
|
10
|
+
return this
|
|
11
|
+
}
|
|
10
12
|
|
|
11
13
|
export const allMocks = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
clearAllMocks: applyAllWrap('mockClear'),
|
|
15
|
+
resetAllMocks: applyAllWrap('mockReset'),
|
|
16
|
+
restoreAllMocks: applyAllWrap('mockRestore'),
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
// We need parent and property for jest.spyOn and mockfn.mockRestore()
|
|
@@ -33,6 +35,7 @@ export const jestfn = (baseimpl, parent, property) => {
|
|
|
33
35
|
queuedMockClear()
|
|
34
36
|
onceStack.length = 0
|
|
35
37
|
mockimpl = noop
|
|
38
|
+
mockname = undefined
|
|
36
39
|
reportedmockimpl = undefined
|
|
37
40
|
fnmock.mockImplementation(mockimpl)
|
|
38
41
|
}
|
|
@@ -66,18 +69,20 @@ export const jestfn = (baseimpl, parent, property) => {
|
|
|
66
69
|
|
|
67
70
|
const queuedMockOnce = (impl) => {
|
|
68
71
|
onceStack.push(impl)
|
|
69
|
-
fnmock.mockImplementation(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
fnmock.mockImplementation(queueImplementation)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const queueImplementation = function (...args) {
|
|
76
|
+
try {
|
|
77
|
+
const impl = onceStack.shift() || mockimpl
|
|
78
|
+
return impl.call(this, ...args)
|
|
79
|
+
} finally {
|
|
80
|
+
// load fast path if we are done with the queue
|
|
81
|
+
if (onceStack.length === 0) {
|
|
82
|
+
assert(mockimpl)
|
|
83
|
+
fnmock.mockImplementation(mockimpl)
|
|
79
84
|
}
|
|
80
|
-
}
|
|
85
|
+
}
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
const jestfnmock = {
|
|
@@ -123,7 +128,7 @@ export const jestfn = (baseimpl, parent, property) => {
|
|
|
123
128
|
case '_isMockFunction':
|
|
124
129
|
return true
|
|
125
130
|
case 'getMockName':
|
|
126
|
-
return () => mockname
|
|
131
|
+
return () => mockname ?? 'jest.fn()'
|
|
127
132
|
case 'mockName':
|
|
128
133
|
return wrap((name) => {
|
|
129
134
|
mockname = name
|
package/src/jest.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { describe, test,
|
|
3
|
-
import { format } from 'node:util'
|
|
2
|
+
import { describe as nodeDescribe, test as nodeTest, afterEach } from 'node:test'
|
|
3
|
+
import { format, types } from 'node:util'
|
|
4
4
|
import { jestfn, allMocks } from './jest.fn.js'
|
|
5
|
-
import { jestmock, requireActual, requireMock } from './jest.mock.js'
|
|
5
|
+
import { jestmock, requireActual, requireMock, resetModules } from './jest.mock.js'
|
|
6
6
|
import * as jestTimers from './jest.timers.js'
|
|
7
7
|
import './jest.snapshot.js'
|
|
8
|
+
import { expect } from 'expect'
|
|
8
9
|
|
|
9
10
|
const makeEach = (impl) => (list) => (template, fn) => {
|
|
10
11
|
for (const args of list) {
|
|
@@ -26,16 +27,38 @@ const makeEach = (impl) => (list) => (template, fn) => {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
const forceExit = process.execArgv.map((x) => x.replaceAll('_', '-')).includes('--test-force-exit')
|
|
31
|
+
|
|
32
|
+
const describe = (...args) => nodeDescribe(...args)
|
|
33
|
+
const test = (name, fn) => {
|
|
34
|
+
if (fn.length > 0) return nodeTest(name, (t, c) => fn(c))
|
|
35
|
+
if (!forceExit) return nodeTest(name, fn)
|
|
36
|
+
return nodeTest(name, async (t) => {
|
|
37
|
+
const res = fn()
|
|
38
|
+
assert(
|
|
39
|
+
types.isPromise(res),
|
|
40
|
+
`Test "${t.fullName}" did not return a Promise or supply a callback, which is required in force-exit mode.
|
|
41
|
+
For tests to not end abruptly, use either async functions (recommended), Promises, or specify callbacks to test() / it().
|
|
42
|
+
Also, using expect.assertions() to ensure the planned number of assertions is being called is advised for async code.`
|
|
43
|
+
)
|
|
44
|
+
return res
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
describe.each = makeEach(describe)
|
|
30
49
|
test.each = makeEach(test)
|
|
31
|
-
|
|
50
|
+
describe.skip = (...args) => nodeDescribe.skip(...args)
|
|
51
|
+
test.skip = (...args) => nodeTest.skip(...args)
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
for (const { error } of expect.extractExpectedAssertionsErrors()) throw error
|
|
55
|
+
})
|
|
32
56
|
|
|
33
57
|
const jest = {
|
|
34
58
|
fn: (impl) => jestfn(impl), // hide extra arguments
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
spyOn: (obj, name) => {
|
|
59
|
+
...allMocks,
|
|
60
|
+
spyOn: (obj, name, accessType) => {
|
|
61
|
+
assert(!accessType, `accessType "${accessType}" is not supported`)
|
|
39
62
|
assert(obj && name && name in obj && !(name in {}) && !(name in Object.prototype))
|
|
40
63
|
const fn = jestfn(obj[name], obj, name)
|
|
41
64
|
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
@@ -45,17 +68,10 @@ const jest = {
|
|
|
45
68
|
mock: jestmock,
|
|
46
69
|
requireMock,
|
|
47
70
|
requireActual,
|
|
71
|
+
resetModules,
|
|
48
72
|
...jestTimers,
|
|
49
73
|
}
|
|
50
74
|
|
|
51
|
-
export { jest }
|
|
75
|
+
export { jest, describe, test, test as it }
|
|
52
76
|
export { expect } from 'expect'
|
|
53
|
-
export {
|
|
54
|
-
beforeEach,
|
|
55
|
-
afterEach,
|
|
56
|
-
before as beforeAll,
|
|
57
|
-
after as afterAll,
|
|
58
|
-
describe,
|
|
59
|
-
test,
|
|
60
|
-
it,
|
|
61
|
-
} from 'node:test'
|
|
77
|
+
export { beforeEach, afterEach, before as beforeAll, after as afterAll } from 'node:test'
|
package/src/jest.mock.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { createRequire, builtinModules } from 'node:module'
|
|
2
|
+
import { createRequire, builtinModules, syncBuiltinESMExports } from 'node:module'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { normalize } from 'node:path'
|
|
3
5
|
import { mock } from 'node:test'
|
|
4
6
|
import { jestfn } from './jest.fn.js'
|
|
5
7
|
|
|
6
|
-
const
|
|
8
|
+
const files = process.argv.slice(1)
|
|
9
|
+
const baseUrl = files.length === 1 && existsSync(files[0]) ? normalize(files[0]) : undefined
|
|
10
|
+
const require = createRequire(baseUrl || import.meta.url)
|
|
7
11
|
const mapMocks = new Map()
|
|
8
12
|
const mapActual = new Map()
|
|
9
13
|
|
|
10
|
-
function resolveModule(name) {
|
|
11
|
-
assert(/^[@a-zA-Z]/u.test(name), 'Mocking relative paths is not
|
|
14
|
+
export function resolveModule(name) {
|
|
15
|
+
assert(baseUrl || /^[@a-zA-Z]/u.test(name), 'Mocking relative paths is not possible')
|
|
12
16
|
return require.resolve(name)
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -25,6 +29,11 @@ export function requireMock(name) {
|
|
|
25
29
|
return mapMocks.get(resolved)
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
export function resetModules() {
|
|
33
|
+
// Caveat: only resets CJS modules, not ESM
|
|
34
|
+
for (const key of Object.keys(require.cache)) delete require.cache[key]
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
const isObject = (obj) => [Object.prototype, null].includes(Object.getPrototypeOf(obj))
|
|
29
38
|
|
|
30
39
|
function override(resolved, lax = false) {
|
|
@@ -41,7 +50,9 @@ function override(resolved, lax = false) {
|
|
|
41
50
|
|
|
42
51
|
// We want to skip overriding frozen properties that already match, e.g. fs.constants
|
|
43
52
|
const filtered = Object.entries(value).filter(([k, v]) => !(k in {}) && current[k] !== v)
|
|
44
|
-
|
|
53
|
+
const access = { configurable: true, enumerable: true, writable: true }
|
|
54
|
+
const definitions = Object.fromEntries(filtered.map(([k, value]) => [k, { value, ...access }]))
|
|
55
|
+
Object.defineProperties(current, definitions)
|
|
45
56
|
if (!lax) assert.deepEqual({ ...current }, value)
|
|
46
57
|
}
|
|
47
58
|
|
|
@@ -86,7 +97,7 @@ export function jestmock(name, mocker) {
|
|
|
86
97
|
|
|
87
98
|
// Loaded ESM: isn't mocked
|
|
88
99
|
// Loaded CJS: mocked via object overriding
|
|
89
|
-
// Loaded built-ins:
|
|
100
|
+
// Loaded built-ins: mocked via object overriding where possible
|
|
90
101
|
// New CJS: mocked via mock.module + require.cache
|
|
91
102
|
// New ESM: mocked via mock.module
|
|
92
103
|
// New built-ins: mocked via mock.module
|
|
@@ -108,15 +119,15 @@ export function jestmock(name, mocker) {
|
|
|
108
119
|
// fall through when e.g. this module doesn't exist or is ESM
|
|
109
120
|
if (Object.hasOwn(require.cache, resolved)) {
|
|
110
121
|
assert.equal(mapActual.get(resolved), require.cache[resolved].exports)
|
|
111
|
-
// If we did't have this prior but have now, it means we just loaded it and there are
|
|
122
|
+
// If we did't have this prior but have now, it means we just loaded it and there are no leaked instances
|
|
112
123
|
if (havePrior) override(resolved)
|
|
113
124
|
require.cache[resolved].exports = value
|
|
114
125
|
} else if (builtinModules.includes(resolved.replace(/^node:/, ''))) {
|
|
115
126
|
override(resolved, true) // Override builtin modules
|
|
127
|
+
syncBuiltinESMExports()
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
mock.module(name, {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
})
|
|
130
|
+
mock.module(name, { defaultExport: value.default, namedExports: value })
|
|
131
|
+
|
|
132
|
+
return this
|
|
122
133
|
}
|
package/src/jest.snapshot.js
CHANGED
|
@@ -58,39 +58,36 @@ function wrapContextName(fn) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return expect(err.message) // jest just checks error message, not error obj
|
|
61
|
+
const throws = (fn, check) =>
|
|
62
|
+
context.assert.throws(fn, (e) => {
|
|
63
|
+
check(e.message) // jest stores only messages for errors
|
|
64
|
+
return true
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const snapInline = (obj, inline) => {
|
|
68
|
+
assert(inline !== undefined, 'Inline Snapshots generation is not supported')
|
|
69
|
+
assert(typeof inline === 'string')
|
|
70
|
+
context.assert.equal(serialize(obj).trim(), inline.trim())
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
assert(inline !== undefined, 'Inline Snapshots generation is not supported')
|
|
77
|
-
assert(typeof inline === 'string')
|
|
78
|
-
return wrap(() => expect(serialize(obj).trim()).toEqual(inline.trim()))
|
|
79
|
-
},
|
|
80
|
-
toMatchSnapshot: (obj) => {
|
|
73
|
+
const snapOnDisk = (obj) =>
|
|
74
|
+
wrapContextName(() => {
|
|
81
75
|
const str = serialize(obj)
|
|
82
76
|
if (!str.includes('\n')) {
|
|
83
77
|
// Node.js always wraps with newlines, while jest wraps only those that are already multiline
|
|
84
78
|
// Hopefully, for simple objects there is no need to use snapshots and those can be just compared directly
|
|
85
|
-
|
|
86
|
-
return { pass: false, message: () => msg }
|
|
79
|
+
throw new Error(`Snapshots of primitives or empty objects/arrays is not supported: ${str}`)
|
|
87
80
|
}
|
|
88
81
|
|
|
89
82
|
maybeSetupJestSnapshots()
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
return context.assert.snapshot(obj)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect.extend({
|
|
87
|
+
toMatchInlineSnapshot: (obj, i) => wrap(() => snapInline(obj, i)),
|
|
88
|
+
toMatchSnapshot: (obj) => wrap(() => snapOnDisk(obj)),
|
|
89
|
+
toThrowErrorMatchingInlineSnapshot: (f, i) => wrap(() => throws(f, (msg) => snapInline(msg, i))),
|
|
90
|
+
toThrowErrorMatchingSnapshot: (f) => wrap(() => throws(f, (msg) => snapOnDisk(msg))),
|
|
94
91
|
})
|
|
95
92
|
|
|
96
93
|
expect.addSnapshotSerializer = (plugin) => plugins.push(plugin)
|
package/src/jest.timers.js
CHANGED
|
@@ -14,12 +14,12 @@ const warnOldTimers = () => {
|
|
|
14
14
|
if (!ok) console.warn('Warning: timer mocks are known to be glitchy before Node.js >=20.11.0')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export function useRealTimers() {
|
|
18
18
|
mock.timers.reset()
|
|
19
|
-
return
|
|
19
|
+
return this
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export
|
|
22
|
+
export function useFakeTimers({ doNotFake = [], ...rest } = {}) {
|
|
23
23
|
assertHaveTimers()
|
|
24
24
|
warnOldTimers()
|
|
25
25
|
assert.deepEqual(rest, {}, 'Unsupported options')
|
|
@@ -33,33 +33,35 @@ export const useFakeTimers = ({ doNotFake = [], ...rest } = {}) => {
|
|
|
33
33
|
if (e.code !== 'ERR_INVALID_STATE') throw e
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
return
|
|
36
|
+
return this
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export
|
|
39
|
+
export function runAllTimers() {
|
|
40
40
|
assertHaveTimers()
|
|
41
41
|
warnOldTimers()
|
|
42
42
|
mock.timers.tick(100_000_000_000) // > 3 years
|
|
43
|
-
return
|
|
43
|
+
return this
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export
|
|
46
|
+
export function runOnlyPendingTimers() {
|
|
47
47
|
const noInfiniteLoopBug = major >= 22 || (major === 20 && minor >= 11)
|
|
48
48
|
assert(noInfiniteLoopBug, 'runOnlyPendingTimers requires Node.js >=20.11.0')
|
|
49
49
|
mock.timers.runAll()
|
|
50
|
-
return
|
|
50
|
+
return this
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export
|
|
53
|
+
export function advanceTimersByTime(time) {
|
|
54
54
|
assertHaveTimers()
|
|
55
55
|
warnOldTimers()
|
|
56
56
|
mock.timers.tick(time)
|
|
57
|
-
return
|
|
57
|
+
return this
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export
|
|
60
|
+
export async function advanceTimersByTimeAsync(time) {
|
|
61
|
+
return this.advanceTimersByTime(time)
|
|
62
|
+
}
|
|
61
63
|
|
|
62
|
-
export
|
|
64
|
+
export function setSystemTime(time) {
|
|
63
65
|
mock.timers.setTime(+time)
|
|
64
|
-
return
|
|
66
|
+
return this
|
|
65
67
|
}
|
package/src/tape.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import assertLoose from 'node:assert'
|
|
2
3
|
import { test } from 'node:test'
|
|
3
4
|
|
|
4
|
-
const knownOptions = new Set(['skip', 'todo', 'concurrency'])
|
|
5
|
+
const knownOptions = new Set(['skip', 'todo', 'concurrency', 'timeout'])
|
|
5
6
|
|
|
6
7
|
function verifyOptions(options) {
|
|
7
8
|
for (const key of Object.keys(options)) {
|
|
@@ -52,7 +53,7 @@ const aliases = {
|
|
|
52
53
|
rejects: ['rejects'],
|
|
53
54
|
doesNotReject: ['resolves'],
|
|
54
55
|
|
|
55
|
-
// specially handled ones as do not exist in t.assert
|
|
56
|
+
// specially handled ones as do not exist in t.assert / assert
|
|
56
57
|
notOk: ['notOk', 'false', 'notok'],
|
|
57
58
|
pass: ['pass'],
|
|
58
59
|
error: ['error', 'ifError', 'ifErr', 'iferror'], // tape
|
|
@@ -61,44 +62,73 @@ const aliases = {
|
|
|
61
62
|
// match/notMatch are confusing as operate on strings in some impls and objs in others. we skip them
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function tapeWrapAssert(t) {
|
|
65
|
-
//
|
|
65
|
+
function tapeWrapAssert(t, callback) {
|
|
66
|
+
// Auto-call api.end() on planned test count reaching zero
|
|
67
|
+
let plan = null
|
|
68
|
+
let count = 0
|
|
69
|
+
const track = (...calls) => {
|
|
70
|
+
count += calls.length
|
|
71
|
+
if (plan === count) api.end()
|
|
72
|
+
if (plan !== null) assert(plan >= count, `plan (${plan}) < count (${count})`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const plannedAssert = () => (plan !== null && t.assert) || assertLoose // t.assert is cached and affected by t.plan
|
|
66
76
|
|
|
77
|
+
// Note: we must use plannedAssert instead of assert everywhere on user calls as we have t.plan
|
|
67
78
|
const api = {
|
|
68
79
|
test: tapeWrap(t.test.bind(t)),
|
|
69
|
-
plan: (
|
|
80
|
+
plan: (total) => {
|
|
81
|
+
assert.equal(typeof total, 'number')
|
|
82
|
+
plan = total
|
|
83
|
+
assert(plan >= count, `plan (${plan}) < count (${count})`)
|
|
84
|
+
if (t.plan) t.plan(plan - count) // plan the remaining tests through node
|
|
85
|
+
track()
|
|
86
|
+
},
|
|
70
87
|
skip: (...r) => t.skip(...r),
|
|
71
88
|
todo: (...r) => t.todo(...r),
|
|
72
89
|
comment: (...r) => t.diagnostic(...r),
|
|
73
|
-
end: () => {
|
|
90
|
+
end: () => {
|
|
91
|
+
if (plan !== null) assert.equal(plan, count, `plan (${plan}) !== count (${count})`)
|
|
92
|
+
if (callback) callback()
|
|
93
|
+
api.end = () => {}
|
|
94
|
+
},
|
|
74
95
|
}
|
|
75
96
|
|
|
76
|
-
// Copy implementations from here if they exist, preferring over
|
|
97
|
+
// Copy implementations from here if they exist, preferring over plannedAssert
|
|
77
98
|
const base = {
|
|
78
|
-
pass: (...r) =>
|
|
79
|
-
notOk: (x, ...r) =>
|
|
80
|
-
error: (err, msg) =>
|
|
81
|
-
assertion: (fn, ...args) => fn.apply(
|
|
99
|
+
pass: (...r) => plannedAssert().ok(true, ...r),
|
|
100
|
+
notOk: (x, ...r) => plannedAssert().ok(!x, ...r),
|
|
101
|
+
error: (err, msg) => plannedAssert().ok(!err, msg || err?.message),
|
|
102
|
+
assertion: (fn, ...args) => fn.apply(plannedAssert(), args),
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
for (const [key, names] of Object.entries(aliases)) {
|
|
85
|
-
const impl = Object.hasOwn(base, key) ? base[key] : (...r) =>
|
|
86
|
-
Object.assign(api, Object.fromEntries(names.map((name) => [name, impl])))
|
|
106
|
+
const impl = Object.hasOwn(base, key) ? base[key] : (...r) => plannedAssert()[key](...r)
|
|
107
|
+
Object.assign(api, Object.fromEntries(names.map((name) => [name, (...r) => track(impl(...r))])))
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
return api
|
|
90
111
|
}
|
|
91
112
|
|
|
113
|
+
const AsyncFunction = (async () => {}).constructor
|
|
114
|
+
|
|
92
115
|
function tapeWrap(test) {
|
|
93
|
-
const tap = (
|
|
116
|
+
const tap = (...args) => {
|
|
94
117
|
const fn = args.pop()
|
|
118
|
+
const name = args.shift() || 'test'
|
|
95
119
|
assert(args.length <= 1)
|
|
96
120
|
const [opts = {}] = args
|
|
97
121
|
verifyOptions(opts)
|
|
98
|
-
|
|
122
|
+
assert(fn instanceof Function)
|
|
123
|
+
if (fn instanceof AsyncFunction) {
|
|
124
|
+
test(name, opts, (t) => fn(tapeWrapAssert(t)))
|
|
125
|
+
} else {
|
|
126
|
+
test(name, opts, (t, callback) => fn(tapeWrapAssert(t, callback)))
|
|
127
|
+
}
|
|
99
128
|
}
|
|
100
129
|
|
|
101
130
|
tap.skip = (...args) => test.skip(...args)
|
|
131
|
+
if (test.only) tap.only = tapeWrap(test.only)
|
|
102
132
|
return tap
|
|
103
133
|
}
|
|
104
134
|
|