@exodus/test 1.0.0-rc.5 → 1.0.0-rc.6
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 +2 -0
- package/bin/index.js +9 -0
- package/package.json +1 -1
- package/src/jest.fn.js +9 -7
- package/src/jest.js +30 -16
- package/src/jest.mock.js +5 -6
- package/src/jest.snapshot.js +21 -24
- package/src/jest.timers.js +15 -13
- package/src/tape.js +40 -13
package/README.md
CHANGED
|
@@ -71,3 +71,5 @@ Just use `"test": "exodus-test"`
|
|
|
71
71
|
- `--passWithNoTests` -- do not error when no test files were found
|
|
72
72
|
|
|
73
73
|
- `--write-snapshots` -- write snapshots instead of verifying them (has `--test-update-snapshots` alias)
|
|
74
|
+
|
|
75
|
+
- `--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
|
@@ -70,6 +70,10 @@ function parseOptions() {
|
|
|
70
70
|
case '--write-snapshots':
|
|
71
71
|
options.writeSnapshots = true
|
|
72
72
|
break
|
|
73
|
+
case '--test-force-exit':
|
|
74
|
+
case '--forceExit':
|
|
75
|
+
options.forceExit = true
|
|
76
|
+
break
|
|
73
77
|
default:
|
|
74
78
|
throw new Error(`Unknown option: ${option}`)
|
|
75
79
|
}
|
|
@@ -111,6 +115,11 @@ if (options.writeSnapshots) {
|
|
|
111
115
|
args.push('--test-update-snapshots')
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
if (options.forceExit) {
|
|
119
|
+
assert((major === 20 && minor > 13) || major >= 22, 'For forceExit, use Node.js >= 20.14.0')
|
|
120
|
+
args.push('--test-force-exit')
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
if (options.coverage) {
|
|
115
124
|
if (options.coverageEngine === 'node') {
|
|
116
125
|
args.push('--experimental-test-coverage')
|
package/package.json
CHANGED
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()
|
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
5
|
import { jestmock, requireActual, requireMock } 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,15 +27,36 @@ 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
|
-
resetAllMocks: () => allMocks.mockReset(),
|
|
37
|
-
restoreAllMocks: () => allMocks.mockRestore(),
|
|
59
|
+
...allMocks,
|
|
38
60
|
spyOn: (obj, name) => {
|
|
39
61
|
assert(obj && name && name in obj && !(name in {}) && !(name in Object.prototype))
|
|
40
62
|
const fn = jestfn(obj[name], obj, name)
|
|
@@ -48,14 +70,6 @@ const jest = {
|
|
|
48
70
|
...jestTimers,
|
|
49
71
|
}
|
|
50
72
|
|
|
51
|
-
export { jest }
|
|
73
|
+
export { jest, describe, test, test as it }
|
|
52
74
|
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'
|
|
75
|
+
export { beforeEach, afterEach, before as beforeAll, after as afterAll } from 'node:test'
|
package/src/jest.mock.js
CHANGED
|
@@ -86,7 +86,7 @@ export function jestmock(name, mocker) {
|
|
|
86
86
|
|
|
87
87
|
// Loaded ESM: isn't mocked
|
|
88
88
|
// Loaded CJS: mocked via object overriding
|
|
89
|
-
// Loaded built-ins:
|
|
89
|
+
// Loaded built-ins: mocked via object overriding where possible
|
|
90
90
|
// New CJS: mocked via mock.module + require.cache
|
|
91
91
|
// New ESM: mocked via mock.module
|
|
92
92
|
// New built-ins: mocked via mock.module
|
|
@@ -108,15 +108,14 @@ export function jestmock(name, mocker) {
|
|
|
108
108
|
// fall through when e.g. this module doesn't exist or is ESM
|
|
109
109
|
if (Object.hasOwn(require.cache, resolved)) {
|
|
110
110
|
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
|
|
111
|
+
// If we did't have this prior but have now, it means we just loaded it and there are no leaked instances
|
|
112
112
|
if (havePrior) override(resolved)
|
|
113
113
|
require.cache[resolved].exports = value
|
|
114
114
|
} else if (builtinModules.includes(resolved.replace(/^node:/, ''))) {
|
|
115
115
|
override(resolved, true) // Override builtin modules
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
mock.module(name, {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
})
|
|
118
|
+
mock.module(name, { defaultExport: value.default, namedExports: value })
|
|
119
|
+
|
|
120
|
+
return this
|
|
122
121
|
}
|
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
|
@@ -52,7 +52,7 @@ const aliases = {
|
|
|
52
52
|
rejects: ['rejects'],
|
|
53
53
|
doesNotReject: ['resolves'],
|
|
54
54
|
|
|
55
|
-
// specially handled ones as do not exist in t.assert
|
|
55
|
+
// specially handled ones as do not exist in t.assert / assert
|
|
56
56
|
notOk: ['notOk', 'false', 'notok'],
|
|
57
57
|
pass: ['pass'],
|
|
58
58
|
error: ['error', 'ifError', 'ifErr', 'iferror'], // tape
|
|
@@ -61,41 +61,68 @@ const aliases = {
|
|
|
61
61
|
// match/notMatch are confusing as operate on strings in some impls and objs in others. we skip them
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function tapeWrapAssert(t) {
|
|
65
|
-
//
|
|
64
|
+
function tapeWrapAssert(t, callback) {
|
|
65
|
+
// Auto-call api.end() on planned test count reaching zero
|
|
66
|
+
let plan = null
|
|
67
|
+
let count = 0
|
|
68
|
+
const track = (...calls) => {
|
|
69
|
+
count += calls.length
|
|
70
|
+
if (plan === count) api.end()
|
|
71
|
+
if (plan !== null) assert(plan >= count, `plan (${plan}) < count (${count})`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const plannedAssert = () => (plan !== null && t.assert) || assert // t.assert is cached and affected by t.plan
|
|
66
75
|
|
|
76
|
+
// Note: we must use plannedAssert instead of assert everywhere on user calls as we have t.plan
|
|
67
77
|
const api = {
|
|
68
78
|
test: tapeWrap(t.test.bind(t)),
|
|
69
|
-
plan: (
|
|
79
|
+
plan: (total) => {
|
|
80
|
+
assert.equal(typeof total, 'number')
|
|
81
|
+
plan = total
|
|
82
|
+
assert(plan >= count, `plan (${plan}) < count (${count})`)
|
|
83
|
+
if (t.plan) t.plan(plan - count) // plan the remaining tests through node
|
|
84
|
+
track()
|
|
85
|
+
},
|
|
70
86
|
skip: (...r) => t.skip(...r),
|
|
71
87
|
todo: (...r) => t.todo(...r),
|
|
72
88
|
comment: (...r) => t.diagnostic(...r),
|
|
73
|
-
end: () => {
|
|
89
|
+
end: () => {
|
|
90
|
+
if (plan !== null) assert.equal(plan, count, `plan (${plan}) !== count (${count})`)
|
|
91
|
+
if (callback) callback()
|
|
92
|
+
api.end = () => {}
|
|
93
|
+
},
|
|
74
94
|
}
|
|
75
95
|
|
|
76
|
-
// Copy implementations from here if they exist, preferring over
|
|
96
|
+
// Copy implementations from here if they exist, preferring over plannedAssert
|
|
77
97
|
const base = {
|
|
78
|
-
pass: (...r) =>
|
|
79
|
-
notOk: (x, ...r) =>
|
|
80
|
-
error: (err, msg) =>
|
|
81
|
-
assertion: (fn, ...args) => fn.apply(
|
|
98
|
+
pass: (...r) => plannedAssert().ok(true, ...r),
|
|
99
|
+
notOk: (x, ...r) => plannedAssert().ok(!x, ...r),
|
|
100
|
+
error: (err, msg) => plannedAssert().ok(!err, msg || err?.message),
|
|
101
|
+
assertion: (fn, ...args) => fn.apply(plannedAssert(), args),
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
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])))
|
|
105
|
+
const impl = Object.hasOwn(base, key) ? base[key] : (...r) => plannedAssert()[key](...r)
|
|
106
|
+
Object.assign(api, Object.fromEntries(names.map((name) => [name, (...r) => track(impl(...r))])))
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
return api
|
|
90
110
|
}
|
|
91
111
|
|
|
112
|
+
const AsyncFunction = (async () => {}).constructor
|
|
113
|
+
|
|
92
114
|
function tapeWrap(test) {
|
|
93
115
|
const tap = (name, ...args) => {
|
|
94
116
|
const fn = args.pop()
|
|
95
117
|
assert(args.length <= 1)
|
|
96
118
|
const [opts = {}] = args
|
|
97
119
|
verifyOptions(opts)
|
|
98
|
-
|
|
120
|
+
assert(fn instanceof Function)
|
|
121
|
+
if (fn instanceof AsyncFunction) {
|
|
122
|
+
test(name, opts, (t) => fn(tapeWrapAssert(t)))
|
|
123
|
+
} else {
|
|
124
|
+
test(name, opts, (t, callback) => fn(tapeWrapAssert(t, callback)))
|
|
125
|
+
}
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
tap.skip = (...args) => test.skip(...args)
|