@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 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
- 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.6",
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(function (...args) {
72
- try {
73
- const impl = onceStack.shift() || mockimpl
74
- return impl.call(this, ...args)
75
- } finally {
76
- // load fast path if we are done with the queue
77
- if (onceStack.length === 0) {
78
- assert(mockimpl)
79
- fnmock.mockImplementation(mockimpl)
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 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
 
@@ -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) || assert // t.assert is cached and affected by t.plan
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 = (name, ...args) => {
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