@exodus/test 1.0.0-rc.6 → 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 +4 -0
- package/bin/index.js +12 -1
- package/bin/jest.js +3 -1
- package/package.json +3 -1
- package/src/jest.fn.js +15 -12
- package/src/jest.js +4 -2
- package/src/jest.mock.js +17 -5
- package/src/tape.js +6 -3
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,8 @@ 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)
|
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
|
|
@@ -120,6 +124,11 @@ if (options.forceExit) {
|
|
|
120
124
|
args.push('--test-force-exit')
|
|
121
125
|
}
|
|
122
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
|
+
|
|
123
132
|
if (options.coverage) {
|
|
124
133
|
if (options.coverageEngine === 'node') {
|
|
125
134
|
args.push('--experimental-test-coverage')
|
|
@@ -181,6 +190,8 @@ if (tsTests.length > 0 && !options.typescript) {
|
|
|
181
190
|
assert(files.length > 0) // otherwise we can run recursively
|
|
182
191
|
args.push(...files)
|
|
183
192
|
|
|
193
|
+
if (!Object.hasOwn(process.env, 'NODE_ENV')) process.env.NODE_ENV = 'test'
|
|
194
|
+
|
|
184
195
|
assert(program && ['node', c8].includes(program))
|
|
185
196
|
const node = spawn(program, args, { stdio: 'inherit' })
|
|
186
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
|
@@ -35,6 +35,7 @@ export const jestfn = (baseimpl, parent, property) => {
|
|
|
35
35
|
queuedMockClear()
|
|
36
36
|
onceStack.length = 0
|
|
37
37
|
mockimpl = noop
|
|
38
|
+
mockname = undefined
|
|
38
39
|
reportedmockimpl = undefined
|
|
39
40
|
fnmock.mockImplementation(mockimpl)
|
|
40
41
|
}
|
|
@@ -68,18 +69,20 @@ export const jestfn = (baseimpl, parent, property) => {
|
|
|
68
69
|
|
|
69
70
|
const queuedMockOnce = (impl) => {
|
|
70
71
|
onceStack.push(impl)
|
|
71
|
-
fnmock.mockImplementation(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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)
|
|
81
84
|
}
|
|
82
|
-
}
|
|
85
|
+
}
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
const jestfnmock = {
|
|
@@ -125,7 +128,7 @@ export const jestfn = (baseimpl, parent, property) => {
|
|
|
125
128
|
case '_isMockFunction':
|
|
126
129
|
return true
|
|
127
130
|
case 'getMockName':
|
|
128
|
-
return () => mockname
|
|
131
|
+
return () => mockname ?? 'jest.fn()'
|
|
129
132
|
case 'mockName':
|
|
130
133
|
return wrap((name) => {
|
|
131
134
|
mockname = name
|
package/src/jest.js
CHANGED
|
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import { describe as nodeDescribe, test as nodeTest, afterEach } from 'node:test'
|
|
3
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
8
|
import { expect } from 'expect'
|
|
@@ -57,7 +57,8 @@ afterEach(() => {
|
|
|
57
57
|
const jest = {
|
|
58
58
|
fn: (impl) => jestfn(impl), // hide extra arguments
|
|
59
59
|
...allMocks,
|
|
60
|
-
spyOn: (obj, name) => {
|
|
60
|
+
spyOn: (obj, name, accessType) => {
|
|
61
|
+
assert(!accessType, `accessType "${accessType}" is not supported`)
|
|
61
62
|
assert(obj && name && name in obj && !(name in {}) && !(name in Object.prototype))
|
|
62
63
|
const fn = jestfn(obj[name], obj, name)
|
|
63
64
|
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
@@ -67,6 +68,7 @@ const jest = {
|
|
|
67
68
|
mock: jestmock,
|
|
68
69
|
requireMock,
|
|
69
70
|
requireActual,
|
|
71
|
+
resetModules,
|
|
70
72
|
...jestTimers,
|
|
71
73
|
}
|
|
72
74
|
|
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
|
|
|
@@ -113,6 +124,7 @@ export function jestmock(name, mocker) {
|
|
|
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
130
|
mock.module(name, { defaultExport: value.default, namedExports: value })
|
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)) {
|
|
@@ -71,7 +72,7 @@ function tapeWrapAssert(t, callback) {
|
|
|
71
72
|
if (plan !== null) assert(plan >= count, `plan (${plan}) < count (${count})`)
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
const plannedAssert = () => (plan !== null && t.assert) ||
|
|
75
|
+
const plannedAssert = () => (plan !== null && t.assert) || assertLoose // t.assert is cached and affected by t.plan
|
|
75
76
|
|
|
76
77
|
// Note: we must use plannedAssert instead of assert everywhere on user calls as we have t.plan
|
|
77
78
|
const api = {
|
|
@@ -112,8 +113,9 @@ function tapeWrapAssert(t, callback) {
|
|
|
112
113
|
const AsyncFunction = (async () => {}).constructor
|
|
113
114
|
|
|
114
115
|
function tapeWrap(test) {
|
|
115
|
-
const tap = (
|
|
116
|
+
const tap = (...args) => {
|
|
116
117
|
const fn = args.pop()
|
|
118
|
+
const name = args.shift() || 'test'
|
|
117
119
|
assert(args.length <= 1)
|
|
118
120
|
const [opts = {}] = args
|
|
119
121
|
verifyOptions(opts)
|
|
@@ -126,6 +128,7 @@ function tapeWrap(test) {
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
tap.skip = (...args) => test.skip(...args)
|
|
131
|
+
if (test.only) tap.only = tapeWrap(test.only)
|
|
129
132
|
return tap
|
|
130
133
|
}
|
|
131
134
|
|