@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 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
- if (mock.module) mock.module('@jest/globals', { defaultExport: globals, namedExports: globals })
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.5",
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
- function applyAll(method) {
7
- assert(['mockClear', 'mockReset', 'mockRestore'].includes(method))
8
- for (const obj of registry) obj[method]()
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
- mockClear: () => applyAll('mockClear'),
13
- mockReset: () => applyAll('mockReset'),
14
- mockRestore: () => applyAll('mockRestore'),
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(function (...args) {
70
- try {
71
- const impl = onceStack.shift() || mockimpl
72
- return impl.call(this, ...args)
73
- } finally {
74
- // load fast path if we are done with the queue
75
- if (onceStack.length === 0) {
76
- assert(mockimpl)
77
- fnmock.mockImplementation(mockimpl)
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, it } from 'node: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
- it.each = makeEach(it)
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
- clearAllMocks: () => allMocks.mockClear(),
36
- resetAllMocks: () => allMocks.mockReset(),
37
- restoreAllMocks: () => allMocks.mockRestore(),
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 require = createRequire(import.meta.url)
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 supported')
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
- Object.assign(current, Object.fromEntries(filtered))
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: not mocked for now (!!!)
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 not leaked instances
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
- defaultExport: value.default,
120
- namedExports: value,
121
- })
130
+ mock.module(name, { defaultExport: value.default, namedExports: value })
131
+
132
+ return this
122
133
  }
@@ -58,39 +58,36 @@ function wrapContextName(fn) {
58
58
  }
59
59
  }
60
60
 
61
- function expectError(fn) {
62
- let err
63
- expect(() => {
64
- try {
65
- fn()
66
- } catch (e) {
67
- err = e
68
- throw e
69
- }
70
- }).toThrow()
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
- expect.extend({
75
- toMatchInlineSnapshot: (obj, inline) => {
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
- const msg = `Snapshotting primitives or empty objects/arrays is not supported yet: ${str}`
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 wrap(() => wrapContextName(() => context.assert.snapshot(obj)))
91
- },
92
- toThrowErrorMatchingInlineSnapshot: (f, i) => wrap(() => expectError(f).toMatchInlineSnapshot(i)),
93
- toThrowErrorMatchingSnapshot: (f) => wrap(() => expectError(f).toMatchSnapshot()),
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)
@@ -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 const useRealTimers = () => {
17
+ export function useRealTimers() {
18
18
  mock.timers.reset()
19
- return jest
19
+ return this
20
20
  }
21
21
 
22
- export const useFakeTimers = ({ doNotFake = [], ...rest } = {}) => {
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 jest
36
+ return this
37
37
  }
38
38
 
39
- export const runAllTimers = () => {
39
+ export function runAllTimers() {
40
40
  assertHaveTimers()
41
41
  warnOldTimers()
42
42
  mock.timers.tick(100_000_000_000) // > 3 years
43
- return jest
43
+ return this
44
44
  }
45
45
 
46
- export const runOnlyPendingTimers = () => {
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 jest
50
+ return this
51
51
  }
52
52
 
53
- export const advanceTimersByTime = (time) => {
53
+ export function advanceTimersByTime(time) {
54
54
  assertHaveTimers()
55
55
  warnOldTimers()
56
56
  mock.timers.tick(time)
57
- return jest
57
+ return this
58
58
  }
59
59
 
60
- export const advanceTimersByTimeAsync = async (time) => jest.advanceTimersByTime(time)
60
+ export async function advanceTimersByTimeAsync(time) {
61
+ return this.advanceTimersByTime(time)
62
+ }
61
63
 
62
- export const setSystemTime = (time) => {
64
+ export function setSystemTime(time) {
63
65
  mock.timers.setTime(+time)
64
- return jest
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
- // Note: we must use t.assert instead of assert everywhere as we have t.plan
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: (...r) => t.plan(...r),
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 t.assert
97
+ // Copy implementations from here if they exist, preferring over plannedAssert
77
98
  const base = {
78
- pass: (...r) => t.assert.ok(true, ...r),
79
- notOk: (x, ...r) => t.assert.ok(!x, ...r),
80
- error: (err, msg) => t.assert.ok(!err, msg || err?.message),
81
- assertion: (fn, ...args) => fn.apply(t.assert, args),
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) => t.assert[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 = (name, ...args) => {
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
- test(name, opts, (t) => fn(tapeWrapAssert(t)))
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