@exodus/test 1.0.0-rc.14 → 1.0.0-rc.16
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/package.json +1 -1
- package/src/dark.cjs +86 -24
- package/src/jest.config.js +29 -23
- package/src/jest.environment.js +2 -54
- package/src/jest.js +3 -1
- package/src/jest.snapshot.js +62 -21
- package/src/tape.js +3 -1
package/package.json
CHANGED
package/src/dark.cjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const { fileURLToPath } = require('node:url')
|
|
2
2
|
|
|
3
|
+
const mayBeUrlToPath = (str) => (str.startsWith('file://') ? fileURLToPath(str) : str)
|
|
4
|
+
|
|
3
5
|
let locForNextTest
|
|
4
6
|
|
|
5
7
|
let installLocationInNextTest = function (loc) {
|
|
@@ -9,9 +11,7 @@ let installLocationInNextTest = function (loc) {
|
|
|
9
11
|
// WARNING
|
|
10
12
|
// Do not refactor, do not wrap
|
|
11
13
|
// This function has to be called unwrapped directly inside our test() impl
|
|
12
|
-
let getCallerLocation
|
|
13
|
-
|
|
14
|
-
const mayBeUrlToPath = (str) => (str.startsWith('file://') ? fileURLToPath(str) : str)
|
|
14
|
+
let getCallerLocation
|
|
15
15
|
|
|
16
16
|
// This is unoptimal
|
|
17
17
|
// Ideally, an option for overriding file locations should be added to Node.js,
|
|
@@ -21,28 +21,90 @@ const mayBeUrlToPath = (str) => (str.startsWith('file://') ? fileURLToPath(str)
|
|
|
21
21
|
// This whole logic is limited only to updating caller locations for reports
|
|
22
22
|
// We don't do use exposed Node.js internas for anything else
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
function createCallerLocationHook() {
|
|
25
|
+
if (getCallerLocation) return { installLocationInNextTest, getCallerLocation }
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const { Test } = require('node:internal/test_runner/test')
|
|
29
|
+
const locStorage = new Map()
|
|
30
|
+
Object.defineProperty(Test.prototype, 'loc', {
|
|
31
|
+
get() {
|
|
32
|
+
return locStorage.get(this)
|
|
33
|
+
},
|
|
34
|
+
set(val) {
|
|
35
|
+
locStorage.set(this, val)
|
|
36
|
+
if (locForNextTest) {
|
|
37
|
+
const loc = locForNextTest
|
|
38
|
+
locForNextTest = undefined
|
|
39
|
+
locStorage.set(this, { line: loc[0], column: loc[1], file: mayBeUrlToPath(loc[2]) })
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// We can replicate getCallerLocation() with public V8 Error CallSite API, but we won't
|
|
45
|
+
// need it anyway if we don't have a path for hook into internal Test implementation
|
|
46
|
+
|
|
47
|
+
const { internalBinding } = require('node:internal/test/binding')
|
|
48
|
+
getCallerLocation = internalBinding('util').getCallerLocation
|
|
49
|
+
} catch {
|
|
50
|
+
getCallerLocation = () => {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { installLocationInNextTest, getCallerLocation }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Easy on Node.js >= 22.3.0, but we polyfill for the rest
|
|
57
|
+
function getTestNamePath(t) {
|
|
58
|
+
// No implementation in Node.js yet, will have to PR
|
|
59
|
+
if (t.fullName) return t.fullName.split(' > ')
|
|
60
|
+
|
|
61
|
+
// We are on Node.js < 22.3.0 where even t.fullName doesn't exist yet, polyfill
|
|
62
|
+
const namePath = Symbol('namePath')
|
|
63
|
+
const getNamePath = Symbol('getNamePath')
|
|
64
|
+
try {
|
|
65
|
+
if (t[namePath]) return t[namePath]
|
|
66
|
+
|
|
67
|
+
// Sigh, ok, whatever
|
|
68
|
+
const { Test } = require('node:internal/test_runner/test')
|
|
69
|
+
|
|
70
|
+
const usePathName = Symbol('usePathName')
|
|
71
|
+
const restoreName = Symbol('restoreName')
|
|
72
|
+
Test.prototype[getNamePath] = function () {
|
|
73
|
+
if (this === this.root) return []
|
|
74
|
+
return [...(this.parent?.[getNamePath]() || []), this.name]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const diagnostic = Test.prototype.diagnostic
|
|
78
|
+
Test.prototype.diagnostic = function (...args) {
|
|
79
|
+
if (args[0] === usePathName) {
|
|
80
|
+
this[restoreName] = this.name
|
|
81
|
+
this.name = this[getNamePath]()
|
|
82
|
+
return
|
|
37
83
|
}
|
|
38
|
-
},
|
|
39
|
-
})
|
|
40
84
|
|
|
41
|
-
|
|
42
|
-
|
|
85
|
+
if (args[0] === restoreName) {
|
|
86
|
+
this.name = this[restoreName]
|
|
87
|
+
delete this[restoreName]
|
|
88
|
+
return
|
|
89
|
+
}
|
|
43
90
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
91
|
+
return diagnostic.apply(this, args)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const TestContextProto = Object.getPrototypeOf(t)
|
|
95
|
+
Object.defineProperty(TestContextProto, namePath, {
|
|
96
|
+
get() {
|
|
97
|
+
this.diagnostic(usePathName)
|
|
98
|
+
const result = this.name
|
|
99
|
+
this.diagnostic(restoreName)
|
|
100
|
+
return result
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return t[namePath]
|
|
105
|
+
} catch {}
|
|
106
|
+
|
|
107
|
+
return [t.name] // last resort
|
|
108
|
+
}
|
|
47
109
|
|
|
48
|
-
module.exports = {
|
|
110
|
+
module.exports = { createCallerLocationHook, getTestNamePath }
|
package/src/jest.config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
3
4
|
import path from 'node:path'
|
|
4
5
|
import { createRequire } from 'node:module'
|
|
5
6
|
import { specialEnvironments } from './jest.environment.js'
|
|
@@ -10,36 +11,39 @@ const baseDir = files.length === 1 ? path.dirname(path.resolve(files[0])) : unde
|
|
|
10
11
|
async function getJestConfig(dir) {
|
|
11
12
|
if (!dir) return
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
const pkg = JSON.parse(await readFile(path.resolve(dir, 'package.json'), 'utf8'))
|
|
14
|
+
const configPath = (ext) => path.resolve(dir, `jest.config.${ext}`)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
let dynamic
|
|
18
|
-
for (const type of ['mjs', 'cjs', 'js']) {
|
|
19
|
-
try {
|
|
20
|
-
const { default: config } = await import(path.resolve(dir, `jest.config.${type}`))
|
|
21
|
-
dynamic = config
|
|
22
|
-
break
|
|
23
|
-
} catch (e) {
|
|
24
|
-
if (e.code !== 'ERR_MODULE_NOT_FOUND') throw e
|
|
25
|
-
}
|
|
26
|
-
}
|
|
16
|
+
assert(!existsSync(configPath('ts')), 'jest.config.ts is not supported yet with .ts extension')
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
const configs = []
|
|
19
|
+
for (const type of ['js', 'ts', 'mjs', 'cjs', 'json']) {
|
|
20
|
+
try {
|
|
21
|
+
if (type === 'json') {
|
|
22
|
+
configs.push(JSON.parse(await readFile(configPath('json'), 'utf8')))
|
|
23
|
+
} else {
|
|
24
|
+
const { default: config } = await import(configPath(type))
|
|
25
|
+
configs.push(config)
|
|
33
26
|
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
if (!['ERR_MODULE_NOT_FOUND', 'ENOENT'].includes(e.code)) throw e
|
|
34
29
|
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(await readFile(path.resolve(dir, 'package.json'), 'utf8'))
|
|
34
|
+
assert(typeof pkg.jest !== 'string', 'String package.json["jest"] values are not supported yet')
|
|
35
|
+
if (pkg.jest) configs.push(pkg.jest)
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (e.code !== 'ENOENT') throw e
|
|
38
|
+
}
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
assert(configs.length < 2, `Multiple jest configs found in ${dir} dir, use only a single one`)
|
|
41
|
+
|
|
42
|
+
if (configs.length > 0) {
|
|
43
|
+
const conf = { ...configs[0] }
|
|
38
44
|
assert(!conf.rootDir, 'Jest config.rootDir is not supported yet')
|
|
39
45
|
conf.rootDir = dir
|
|
40
46
|
return conf
|
|
41
|
-
} catch (e) {
|
|
42
|
-
if (e.code !== 'ENOENT') throw e
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const parent = path.dirname(dir)
|
|
@@ -124,7 +128,9 @@ export async function installJestEnvironment(jestGlobals) {
|
|
|
124
128
|
if (c.restoreMocks) beforeEach(() => jest.restoreAllMocks())
|
|
125
129
|
if (c.resetModules) beforeEach(() => jest.resetModules())
|
|
126
130
|
|
|
127
|
-
const require =
|
|
131
|
+
const require = config.rootDir
|
|
132
|
+
? createRequire(path.resolve(config.rootDir, 'package.json'))
|
|
133
|
+
: () => assert.fail('Unreachable: requiring plugins without a rootDir')
|
|
128
134
|
|
|
129
135
|
if (Object.hasOwn(specialEnvironments, c.testEnvironment)) {
|
|
130
136
|
specialEnvironments[c.testEnvironment](require, jestGlobals, c.testEnvironmentOptions)
|
package/src/jest.environment.js
CHANGED
|
@@ -1,56 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
function getTestNamePath(t, { require } = {}) {
|
|
4
|
-
// No implementation in Node.js yet, will have to PR
|
|
5
|
-
if (t.fullName) return t.fullName.split(' > ')
|
|
6
|
-
|
|
7
|
-
// We are on Node.js < 22.3.0 where even t.fullName doesn't exist yet, polyfill
|
|
8
|
-
const namePath = Symbol('namePath')
|
|
9
|
-
try {
|
|
10
|
-
if (t[namePath]) return t[namePath]
|
|
11
|
-
|
|
12
|
-
// Sigh, ok, whatever
|
|
13
|
-
const { Test } = require('node:internal/test_runner/test')
|
|
14
|
-
|
|
15
|
-
const usePathName = Symbol('usePathName')
|
|
16
|
-
const restoreName = Symbol('restoreName')
|
|
17
|
-
Test.prototype.getNamePath = function () {
|
|
18
|
-
if (this === this.root) return []
|
|
19
|
-
return [...(this.parent?.getNamePath() || []), this.name]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const diagnostic = Test.prototype.diagnostic
|
|
23
|
-
Test.prototype.diagnostic = function (...args) {
|
|
24
|
-
if (args[0] === usePathName) {
|
|
25
|
-
this[restoreName] = this.name
|
|
26
|
-
this.name = this.getNamePath()
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (args[0] === restoreName) {
|
|
31
|
-
this.name = this[restoreName]
|
|
32
|
-
delete this[restoreName]
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return diagnostic.apply(this, args)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const TestContextProto = Object.getPrototypeOf(t)
|
|
40
|
-
Object.defineProperty(TestContextProto, namePath, {
|
|
41
|
-
get() {
|
|
42
|
-
this.diagnostic(usePathName)
|
|
43
|
-
const result = this.name
|
|
44
|
-
this.diagnostic(restoreName)
|
|
45
|
-
return result
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
return t[namePath]
|
|
50
|
-
} catch {}
|
|
51
|
-
|
|
52
|
-
return [t.name] // last resort
|
|
53
|
-
}
|
|
1
|
+
import { getTestNamePath } from './dark.cjs'
|
|
54
2
|
|
|
55
3
|
export const specialEnvironments = {
|
|
56
4
|
__proto__: null,
|
|
@@ -80,7 +28,7 @@ export const specialEnvironments = {
|
|
|
80
28
|
|
|
81
29
|
jestGlobals.beforeEach((t) => {
|
|
82
30
|
if (!pollyGlobals.isPollyActive) return
|
|
83
|
-
const name = getTestNamePath(t
|
|
31
|
+
const name = getTestNamePath(t).join('/')
|
|
84
32
|
pollyGlobals.pollyContext.polly = new Polly(name, pollyGlobals.pollyContext.options)
|
|
85
33
|
})
|
|
86
34
|
|
package/src/jest.js
CHANGED
|
@@ -6,10 +6,12 @@ import { jestfn, allMocks } from './jest.fn.js'
|
|
|
6
6
|
import { jestmock, requireActual, requireMock, resetModules } from './jest.mock.js'
|
|
7
7
|
import * as jestTimers from './jest.timers.js'
|
|
8
8
|
import './jest.snapshot.js'
|
|
9
|
-
import {
|
|
9
|
+
import { createCallerLocationHook } from './dark.cjs'
|
|
10
10
|
import { expect } from 'expect'
|
|
11
11
|
import matchers from 'jest-extended'
|
|
12
12
|
|
|
13
|
+
const { getCallerLocation, installLocationInNextTest } = createCallerLocationHook()
|
|
14
|
+
|
|
13
15
|
expect.extend(matchers)
|
|
14
16
|
|
|
15
17
|
let defaultTimeout = jestConfig().testTimeout // overridable via jest.setTimeout()
|
package/src/jest.snapshot.js
CHANGED
|
@@ -3,16 +3,25 @@ import { createRequire } from 'node:module'
|
|
|
3
3
|
import { expect } from 'expect'
|
|
4
4
|
import { format } from 'pretty-format'
|
|
5
5
|
import assert from 'node:assert/strict'
|
|
6
|
-
import { basename, dirname, join } from 'node:path'
|
|
6
|
+
import { basename, dirname, join, normalize } from 'node:path'
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
7
8
|
import { jestConfig } from './jest.config.js'
|
|
8
9
|
import { relativeRequire } from './jest.mock.js'
|
|
10
|
+
import { getTestNamePath } from './dark.cjs'
|
|
9
11
|
|
|
10
12
|
const { snapshotFormat, snapshotSerializers } = jestConfig()
|
|
11
13
|
const plugins = []
|
|
12
14
|
const serialize = (val) => format(val, { ...snapshotFormat, plugins }).replaceAll(/\r\n|\r/gu, '\n')
|
|
15
|
+
const resolveSnapshot = (f) => join(dirname(f), '__snapshots__', `${basename(f)}.snap`)
|
|
13
16
|
|
|
14
17
|
let serializersAreSetup = false
|
|
15
|
-
let snapshotsAreJest
|
|
18
|
+
let snapshotsAreJest
|
|
19
|
+
|
|
20
|
+
// For manually loading the snapshot
|
|
21
|
+
const files = process.argv.slice(1)
|
|
22
|
+
const snapshotLocation = files.length === 1 ? resolveSnapshot(normalize(files[0])) : undefined
|
|
23
|
+
const nameCounts = new Map()
|
|
24
|
+
let snapshotText
|
|
16
25
|
|
|
17
26
|
function maybeSetupSerializers() {
|
|
18
27
|
if (serializersAreSetup) return
|
|
@@ -23,14 +32,20 @@ function maybeSetupSerializers() {
|
|
|
23
32
|
|
|
24
33
|
// We want to setup snapshots to behave like jest only when first used from jest API
|
|
25
34
|
function maybeSetupJestSnapshots() {
|
|
26
|
-
if (snapshotsAreJest) return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
if (snapshotsAreJest !== undefined) return snapshotsAreJest
|
|
36
|
+
try {
|
|
37
|
+
maybeSetupSerializers()
|
|
38
|
+
const require = createRequire(import.meta.url)
|
|
39
|
+
const { snapshot } = require('node:test') // attempt to load them, and we need to do that synchronously
|
|
40
|
+
assert(snapshot, 'snapshots require Node.js >=22.3.0')
|
|
41
|
+
snapshot.setDefaultSnapshotSerializers([serialize])
|
|
42
|
+
snapshot.setResolveSnapshotPath(resolveSnapshot)
|
|
43
|
+
snapshotsAreJest = true
|
|
44
|
+
} catch {
|
|
45
|
+
snapshotsAreJest = false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return snapshotsAreJest
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
const wrap = (check) => {
|
|
@@ -91,22 +106,48 @@ const snapInline = (obj, inline) => {
|
|
|
91
106
|
getAssert().strictEqual(serialize(obj).trim(), inline.trim())
|
|
92
107
|
}
|
|
93
108
|
|
|
94
|
-
const snapOnDisk = (obj) =>
|
|
95
|
-
|
|
96
|
-
maybeSetupJestSnapshots()
|
|
109
|
+
const snapOnDisk = (obj) => {
|
|
110
|
+
const escape = (str) => str.replaceAll(/([\\`])/gu, '\\$1')
|
|
97
111
|
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
if (!maybeSetupJestSnapshots()) {
|
|
113
|
+
// We don't have native snapshots, polyfill reading
|
|
114
|
+
if (snapshotLocation && snapshotText !== null) {
|
|
100
115
|
try {
|
|
101
|
-
|
|
102
|
-
} catch
|
|
103
|
-
|
|
104
|
-
throw e
|
|
116
|
+
snapshotText = `\n${readFileSync(snapshotLocation, 'utf8')}\n` // we'll search wrapped in \n
|
|
117
|
+
} catch {
|
|
118
|
+
snapshotText = null
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
const addFail = `Adding new snapshots requires Node.js >=22.3.0`
|
|
123
|
+
|
|
124
|
+
// We don't support polyfilled snapshot generation here, only parsing
|
|
125
|
+
// Also be careful with assertion plan counters
|
|
126
|
+
if (!snapshotText) getAssert().fail(`Could not find snapshot file. ${addFail}`)
|
|
127
|
+
|
|
128
|
+
const name = getTestNamePath(context).join(' ')
|
|
129
|
+
const count = (nameCounts.get(name) || 0) + 1
|
|
130
|
+
nameCounts.set(name, count)
|
|
131
|
+
const escaped = escape(serialize(obj))
|
|
132
|
+
const key = `${name} ${count}`
|
|
133
|
+
const makeEntry = (x) => `\nexports[\`${escape(key)}\`] = \`${x}\`;\n`
|
|
134
|
+
const final = escaped.includes('\n') ? `\n${escaped}\n` : escaped
|
|
135
|
+
if (snapshotText.includes(makeEntry(final))) return
|
|
136
|
+
// Perhaps wrapped with newlines from Node.js snapshots?
|
|
137
|
+
if (!final.includes('\n') && snapshotText.includes(makeEntry(`\n${final}\n`))) return
|
|
138
|
+
return getAssert().fail(`Could not match "${key}" in snapshot. ${addFail}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Node.js always wraps with newlines, while jest wraps only those that are already multiline
|
|
142
|
+
try {
|
|
143
|
+
wrapContextName(() => getAssert().snapshot(obj))
|
|
144
|
+
} catch (e) {
|
|
145
|
+
const escaped = escape(e.expected)
|
|
146
|
+
const final = escaped.includes('\n') ? escaped : `\n${escaped}\n`
|
|
147
|
+
if (final === e.actual) return
|
|
148
|
+
throw e
|
|
149
|
+
}
|
|
150
|
+
}
|
|
110
151
|
|
|
111
152
|
expect.extend({
|
|
112
153
|
toMatchInlineSnapshot: (obj, i) => wrap(() => snapInline(obj, i)),
|
package/src/tape.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import assertLoose from 'node:assert'
|
|
3
3
|
import { test } from 'node:test'
|
|
4
|
-
import {
|
|
4
|
+
import { createCallerLocationHook } from './dark.cjs'
|
|
5
5
|
|
|
6
6
|
const knownOptions = new Set(['skip', 'todo', 'concurrency', 'timeout'])
|
|
7
7
|
|
|
@@ -134,6 +134,8 @@ function tapeWrapAssert(t, callback) {
|
|
|
134
134
|
|
|
135
135
|
const AsyncFunction = (async () => {}).constructor
|
|
136
136
|
|
|
137
|
+
const { getCallerLocation, installLocationInNextTest } = createCallerLocationHook()
|
|
138
|
+
|
|
137
139
|
function tapeWrap(test) {
|
|
138
140
|
const tap = (...args) => {
|
|
139
141
|
const fn = args.pop()
|