@exodus/test 1.0.0-rc.16 → 1.0.0-rc.17

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
@@ -58,9 +58,7 @@ Just use `"test": "exodus-test"`
58
58
 
59
59
  - `--jest` -- register jest test helpers as global variables, also load `jest.config.*` configuration options
60
60
 
61
- - `--typescript` -- use typescript loader (which also compiles esm to cjs where needed)
62
-
63
- - `--esbuild` -- use esbuild loader (currently an alias for `--typescript`)
61
+ - `--esbuild` -- use esbuild loader, also enables Typescript support
64
62
 
65
63
  - `--babel` -- use babel loader (slower than `--esbuild`, makes sense if you have a special config)
66
64
 
package/bin/index.js CHANGED
@@ -9,7 +9,8 @@ import glob from 'fast-glob'
9
9
 
10
10
  const bindir = dirname(fileURLToPath(import.meta.url))
11
11
 
12
- const DEFAULT_PATTERNS = ['**/?(*.)+(spec|test).?([cm])[jt]s?(x)']
12
+ const EXTS = `.?([cm])[jt]s?(x)` // we differt from jest, allowing [cm] before everything
13
+ const DEFAULT_PATTERNS = [`**/__tests__/**/*${EXTS}`, `**/?(*.)+(spec|test)${EXTS}`]
13
14
 
14
15
  function versionCheck() {
15
16
  const [major, minor, patch] = process.versions.node.split('.').map(Number)
@@ -51,7 +52,9 @@ function parseOptions() {
51
52
  options.jest = true
52
53
  break
53
54
  case '--typescript':
55
+ console.warn('Option --typescript is going to be gone or changed. Use --esbuild instead')
54
56
  options.typescript = true
57
+ options.esbuild = true
55
58
  break
56
59
  case '--esbuild':
57
60
  options.esbuild = true
@@ -147,7 +150,7 @@ if (options.coverage) {
147
150
  }
148
151
  }
149
152
 
150
- if (options.typescript || options.esbuild) {
153
+ if (options.esbuild) {
151
154
  if (major >= 22 || (major === 20 && minor >= 6) || (major === 18 && minor >= 18)) {
152
155
  assert(resolveImport)
153
156
  args.push('--import', resolveImport('tsx'))
@@ -157,11 +160,12 @@ if (options.typescript || options.esbuild) {
157
160
  }
158
161
 
159
162
  if (options.babel) {
160
- assert(!options.typescript, 'Options --babel and --typescript are mutually exclusive')
163
+ assert(!options.esbuild, 'Options --babel and --esbuild are mutually exclusive')
161
164
  args.push('-r', resolveRequire('./babel.cjs'))
162
165
  }
163
166
 
164
167
  const ignore = ['**/node_modules']
168
+ let filter
165
169
  if (process.env.EXODUS_TEST_IGNORE) {
166
170
  // fast-glob treats negative ignore patterns exactly the same as positive, let's not cause a confusion
167
171
  assert(!process.env.EXODUS_TEST_IGNORE.startsWith('!'), 'Ignore pattern should not be negative')
@@ -172,7 +176,6 @@ if (process.env.EXODUS_TEST_IGNORE) {
172
176
  if (options.jest) {
173
177
  const { loadJestConfig } = await import('../src/jest.config.js')
174
178
  const config = await loadJestConfig(process.cwd())
175
- ignore.push(...config.testPathIgnorePatterns)
176
179
  if (major >= 20 || (major === 18 && minor >= 18)) {
177
180
  args.push('--import', resolve(bindir, 'jest.js'))
178
181
  } else {
@@ -189,12 +192,30 @@ if (options.jest) {
189
192
  })
190
193
  }
191
194
 
192
- const jestPatterns = Array.isArray(config.testMatch) ? config.testMatch : [config.testMatch]
193
- if (patterns.length === 0) patterns.push(...jestPatterns) // no patterns specified via argv
195
+ if (patterns.length > 0) {
196
+ // skip, we already have patterns via argv
197
+ } else if (config.testRegex) {
198
+ assert(typeof config.testRegex === 'string', `config.testRegex should be a string`)
199
+ assert(!config.testMatch, 'config.testRegex can not be used together with config.testMatch')
200
+ patterns.push('**/*')
201
+ } else if (config.testMatch) {
202
+ patterns.push(...(Array.isArray(config.testMatch) ? config.testMatch : [config.testMatch]))
203
+ }
204
+
205
+ const testRegex = config.testRegex ? new RegExp(config.testRegex, 'u') : null
206
+ const ignoreRegexes = config.testPathIgnorePatterns.map((x) => new RegExp(x, 'u'))
207
+ if (testRegex || ignoreRegexes.length > 0) {
208
+ filter = (x) => {
209
+ const resolved = `<rootDir>/${x}` // don't actually include cwd, that should be irrelevant
210
+ if (testRegex && !testRegex.test(resolved)) return false
211
+ return !ignoreRegexes.some((r) => r.test(resolved))
212
+ }
213
+ }
194
214
  }
195
215
 
196
216
  if (patterns.length === 0) patterns.push(...DEFAULT_PATTERNS) // defaults
197
- const allfiles = await glob(patterns, { ignore })
217
+ const globbed = await glob(patterns, { ignore })
218
+ const allfiles = filter ? globbed.filter(filter) : globbed
198
219
 
199
220
  if (allfiles.length === 0) {
200
221
  if (options.passWithNoTests) {
@@ -243,13 +264,13 @@ files.sort((a, b) => {
243
264
  })
244
265
 
245
266
  if (options.debug.files) {
246
- console.log(files.join('\n'))
267
+ for (const f of files) console.log(f) // joining with \n can get truncated, too big
247
268
  process.exit(1) // do not succeed!
248
269
  }
249
270
 
250
271
  const tsTests = files.filter((file) => /\.[mc]?tsx?$/u.test(file))
251
- if (tsTests.length > 0 && !options.typescript) {
252
- console.error(`Some tests require --typescript flag:\n ${tsTests.join('\n ')}`)
272
+ if (tsTests.length > 0 && !options.esbuild) {
273
+ console.error(`Some tests require --esbuild flag:\n ${tsTests.join('\n ')}`)
253
274
  process.exit(1)
254
275
  } else if (!allfiles.some((file) => file.endsWith('.ts')) && options.typescript) {
255
276
  console.warn(`Flag --typescript has been used, but there were no TypeScript tests found!`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/test",
3
- "version": "1.0.0-rc.16",
3
+ "version": "1.0.0-rc.17",
4
4
  "author": "Exodus Movement, Inc.",
5
5
  "description": "A test suite runner",
6
6
  "homepage": "https://github.com/ExodusMovement/test",
@@ -46,9 +46,9 @@
46
46
  "CHANGELOG.md"
47
47
  ],
48
48
  "scripts": {
49
- "test": "./bin/index.js --jest --typescript",
49
+ "test": "./bin/index.js --jest --esbuild",
50
50
  "test:tape": "./bin/index.js --esbuild '__test__/tape/test/*.js' __test__/tape.test.js",
51
- "coverage": "./bin/index.js --jest --typescript --coverage",
51
+ "coverage": "./bin/index.js --jest --esbuild --coverage",
52
52
  "lint": "prettier --list-different . && eslint .",
53
53
  "lint:fix": "prettier --write . && eslint --fix ."
54
54
  },
@@ -53,7 +53,6 @@ async function getJestConfig(dir) {
53
53
  const normalizeJestConfig = (config) => ({
54
54
  testEnvironment: 'node',
55
55
  testTimeout: 5000,
56
- testMatch: ['**/__tests__/**/*.?([cm])[jt]s?(x)', '**/?(*.)+(spec|test).?([cm])[jt]s?(x)'],
57
56
  testPathIgnorePatterns: [],
58
57
  snapshotSerializers: [],
59
58
  injectGlobals: true,
@@ -93,7 +92,7 @@ function verifyJestConfig(c) {
93
92
  assert(!c.preset, 'Jest config.preset is not supported')
94
93
 
95
94
  // TODO
96
- const TODO = ['globalSetup', 'globalTeardown', 'randomize', 'projects', 'roots', 'testRegex']
95
+ const TODO = ['globalSetup', 'globalTeardown', 'randomize', 'projects', 'roots']
97
96
  TODO.push('resolver', 'unmockedModulePathPatterns', 'watchPathIgnorePatterns', 'snapshotResolver')
98
97
  for (const key of TODO) assert.equal(c[key], undefined, `Jest config.${key} is not supported yet`)
99
98
  }
package/src/jest.js CHANGED
@@ -15,6 +15,7 @@ const { getCallerLocation, installLocationInNextTest } = createCallerLocationHoo
15
15
  expect.extend(matchers)
16
16
 
17
17
  let defaultTimeout = jestConfig().testTimeout // overridable via jest.setTimeout()
18
+ const defaultConcurrency = jestConfig().maxConcurrency
18
19
 
19
20
  function parseArgs(list, targs) {
20
21
  if (!(Object.isFrozen(list) && list.length === targs.length + 1)) return list // template check
@@ -79,10 +80,37 @@ const makeEach =
79
80
 
80
81
  const forceExit = process.execArgv.map((x) => x.replaceAll('_', '-')).includes('--test-force-exit')
81
82
 
82
- const describe = (...args) => nodeDescribe(...args)
83
- const test = (name, fn, testTimeout) => {
83
+ const inConcurrent = []
84
+ const concurrent = []
85
+ const describe = (...args) => {
86
+ const fn = args.pop()
87
+ const optionsConcurrent = args?.at(-1)?.concurrency > 1
88
+ if (optionsConcurrent) inConcurrent.push(fn)
89
+ const result = nodeDescribe(...args, async () => {
90
+ const res = fn()
91
+
92
+ // We do only block-level concurrency, not file-level
93
+ if (concurrent.length === 1) {
94
+ const [args, callerLocation] = concurrent[0]
95
+ testRaw(callerLocation, ...args)
96
+ concurrent.length = 0
97
+ } else if (concurrent.length > 0) {
98
+ const queue = [...concurrent]
99
+ concurrent.length = 0
100
+ describe('concurrent', { concurrency: defaultConcurrency }, () => {
101
+ for (const [args, callerLocation] of queue) testRaw(callerLocation, ...args)
102
+ })
103
+ }
104
+
105
+ return res
106
+ })
107
+ if (optionsConcurrent) inConcurrent.pop()
108
+ return result
109
+ }
110
+
111
+ const testRaw = (callerLocation, name, fn, testTimeout) => {
84
112
  const timeout = testTimeout ?? defaultTimeout
85
- installLocationInNextTest(getCallerLocation())
113
+ installLocationInNextTest(callerLocation)
86
114
  if (fn.length > 0) return nodeTest(name, (t, c) => fn(c))
87
115
  if (!forceExit) return nodeTest(name, fn)
88
116
  return nodeTest(name, { timeout }, async (t) => {
@@ -97,8 +125,13 @@ Also, using expect.assertions() to ensure the planned number of assertions is be
97
125
  })
98
126
  }
99
127
 
128
+ const test = (...args) => testRaw(getCallerLocation(), ...args)
129
+
100
130
  describe.each = makeEach(describe)
101
131
  test.each = makeEach(test)
132
+ test.concurrent = (...args) =>
133
+ inConcurrent.length > 0 ? test(...args) : concurrent.push([args, getCallerLocation()])
134
+ test.concurrent.each = makeEach(test.concurrent)
102
135
  describe.skip = (...args) => nodeDescribe.skip(...args)
103
136
  test.skip = (...args) => nodeTest.skip(...args)
104
137