@remix-run/test 0.2.0 → 0.4.0
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 +43 -44
- package/dist/app/client/entry.js +4 -0
- package/dist/app/server.d.ts.map +1 -1
- package/dist/app/server.js +10 -10
- package/dist/cli.d.ts +30 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +87 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/config.d.ts +55 -21
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +82 -33
- package/dist/lib/context.d.ts +5 -5
- package/dist/lib/coverage-loader.js +2 -2
- package/dist/lib/coverage.js +1 -1
- package/dist/lib/fake-timers.d.ts +39 -0
- package/dist/lib/fake-timers.d.ts.map +1 -1
- package/dist/lib/fake-timers.js +27 -8
- package/dist/lib/framework.d.ts +12 -6
- package/dist/lib/framework.d.ts.map +1 -1
- package/dist/lib/framework.js +24 -12
- package/dist/lib/import-module.d.ts.map +1 -1
- package/dist/lib/import-module.js +13 -3
- package/dist/lib/reporters/dot.d.ts.map +1 -1
- package/dist/lib/reporters/dot.js +10 -0
- package/dist/lib/reporters/files.d.ts.map +1 -1
- package/dist/lib/reporters/files.js +10 -0
- package/dist/lib/reporters/results.d.ts +1 -1
- package/dist/lib/reporters/results.d.ts.map +1 -1
- package/dist/lib/reporters/spec.d.ts.map +1 -1
- package/dist/lib/reporters/spec.js +10 -0
- package/dist/lib/reporters/tap.d.ts.map +1 -1
- package/dist/lib/reporters/tap.js +10 -0
- package/dist/lib/runner-browser.d.ts.map +1 -1
- package/dist/lib/runner-browser.js +40 -2
- package/dist/lib/runner.d.ts +18 -1
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +187 -38
- package/dist/lib/worker-e2e-file.d.ts +11 -0
- package/dist/lib/worker-e2e-file.d.ts.map +1 -0
- package/dist/lib/worker-e2e-file.js +69 -0
- package/dist/lib/worker-e2e.js +11 -47
- package/dist/lib/worker-process.d.ts +2 -0
- package/dist/lib/worker-process.d.ts.map +1 -0
- package/dist/lib/worker-process.js +55 -0
- package/dist/lib/worker-results.d.ts +3 -0
- package/dist/lib/worker-results.d.ts.map +1 -0
- package/dist/lib/worker-results.js +20 -0
- package/dist/lib/worker-server.d.ts +10 -0
- package/dist/lib/worker-server.d.ts.map +1 -0
- package/dist/lib/worker-server.js +112 -0
- package/dist/lib/worker.js +6 -55
- package/package.json +5 -5
- package/src/app/client/entry.ts +4 -0
- package/src/app/server.ts +11 -10
- package/src/cli.ts +121 -28
- package/src/index.ts +1 -1
- package/src/lib/config.ts +144 -58
- package/src/lib/context.ts +5 -5
- package/src/lib/coverage-loader.ts +2 -2
- package/src/lib/coverage.ts +1 -1
- package/src/lib/fake-timers.ts +65 -8
- package/src/lib/framework.ts +53 -36
- package/src/lib/import-module.ts +14 -3
- package/src/lib/reporters/dot.ts +9 -0
- package/src/lib/reporters/files.ts +9 -0
- package/src/lib/reporters/results.ts +1 -1
- package/src/lib/reporters/spec.ts +9 -0
- package/src/lib/reporters/tap.ts +9 -0
- package/src/lib/runner-browser.ts +46 -2
- package/src/lib/runner.ts +253 -50
- package/src/lib/ts-transform.ts +1 -1
- package/src/lib/worker-e2e-file.ts +98 -0
- package/src/lib/worker-e2e.ts +14 -51
- package/src/lib/worker-process.ts +69 -0
- package/src/lib/worker-results.ts +22 -0
- package/src/lib/worker-server.ts +123 -0
- package/src/lib/worker.ts +7 -47
- package/tsconfig.json +6 -3
package/src/lib/config.ts
CHANGED
|
@@ -44,19 +44,23 @@ const cliOptions = {
|
|
|
44
44
|
},
|
|
45
45
|
'glob.browser': {
|
|
46
46
|
type: 'string',
|
|
47
|
-
|
|
47
|
+
multiple: true,
|
|
48
|
+
description: 'Glob pattern(s) for browser test files',
|
|
48
49
|
},
|
|
49
50
|
'glob.e2e': {
|
|
50
51
|
type: 'string',
|
|
51
|
-
|
|
52
|
+
multiple: true,
|
|
53
|
+
description: 'Glob pattern(s) for E2E test files',
|
|
52
54
|
},
|
|
53
55
|
'glob.exclude': {
|
|
54
56
|
type: 'string',
|
|
55
|
-
|
|
57
|
+
multiple: true,
|
|
58
|
+
description: 'Glob pattern(s) for paths to exclude from discovery',
|
|
56
59
|
},
|
|
57
60
|
'glob.test': {
|
|
58
61
|
type: 'string',
|
|
59
|
-
|
|
62
|
+
multiple: true,
|
|
63
|
+
description: 'Glob pattern(s) for all test files',
|
|
60
64
|
},
|
|
61
65
|
concurrency: {
|
|
62
66
|
type: 'string',
|
|
@@ -113,7 +117,12 @@ const cliOptions = {
|
|
|
113
117
|
project: {
|
|
114
118
|
type: 'string',
|
|
115
119
|
short: 'p',
|
|
116
|
-
|
|
120
|
+
multiple: true,
|
|
121
|
+
description: 'Filter to specific Playwright project(s)',
|
|
122
|
+
},
|
|
123
|
+
pool: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'Pool used to run server and E2E test files: forks, threads (default: forks)',
|
|
117
126
|
},
|
|
118
127
|
reporter: {
|
|
119
128
|
type: 'string',
|
|
@@ -123,7 +132,8 @@ const cliOptions = {
|
|
|
123
132
|
type: {
|
|
124
133
|
type: 'string',
|
|
125
134
|
short: 't',
|
|
126
|
-
|
|
135
|
+
multiple: true,
|
|
136
|
+
description: 'Test types to run (default: server, browser, e2e)',
|
|
127
137
|
},
|
|
128
138
|
watch: {
|
|
129
139
|
type: 'boolean',
|
|
@@ -148,19 +158,33 @@ const defaultValues: ResolvedRemixTestConfig = {
|
|
|
148
158
|
functions: undefined,
|
|
149
159
|
},
|
|
150
160
|
glob: {
|
|
151
|
-
test: '**/*.test{,.e2e,.browser}.{ts,tsx}',
|
|
152
|
-
browser: '**/*.test.browser.{ts,tsx}',
|
|
153
|
-
e2e: '**/*.test.e2e.{ts,tsx}',
|
|
154
|
-
exclude: 'node_modules/**',
|
|
161
|
+
test: ['**/*.test{,.e2e,.browser}.{ts,tsx}'],
|
|
162
|
+
browser: ['**/*.test.browser.{ts,tsx}'],
|
|
163
|
+
e2e: ['**/*.test.e2e.{ts,tsx}'],
|
|
164
|
+
exclude: ['node_modules/**'],
|
|
155
165
|
},
|
|
156
|
-
|
|
157
|
-
type: 'server,browser,e2e',
|
|
158
|
-
setup: undefined,
|
|
166
|
+
pool: 'forks',
|
|
159
167
|
playwrightConfig: undefined,
|
|
160
168
|
project: undefined,
|
|
169
|
+
reporter: process.env.CI === 'true' ? 'files' : 'spec',
|
|
170
|
+
setup: undefined,
|
|
171
|
+
type: ['server', 'browser', 'e2e'],
|
|
161
172
|
watch: false,
|
|
162
|
-
}
|
|
173
|
+
}
|
|
163
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Worker pool used by `remix-test` to run server and E2E test files.
|
|
177
|
+
* `'forks'` (default) uses child processes for stronger isolation; `'threads'`
|
|
178
|
+
* uses worker threads for projects that prefer lower-overhead startup.
|
|
179
|
+
*/
|
|
180
|
+
export type RemixTestPool = 'forks' | 'threads'
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* User-facing configuration for the `remix-test` CLI. Every field is
|
|
184
|
+
* optional — unset fields fall back to runner defaults. The same shape can
|
|
185
|
+
* be exported from a config file (see `--config`) or passed inline to
|
|
186
|
+
* {@link runRemixTest} via the corresponding flags.
|
|
187
|
+
*/
|
|
164
188
|
export interface RemixTestConfig {
|
|
165
189
|
/**
|
|
166
190
|
* Options for controlling the playwright browser
|
|
@@ -172,17 +196,18 @@ export interface RemixTestConfig {
|
|
|
172
196
|
open?: boolean
|
|
173
197
|
}
|
|
174
198
|
/**
|
|
175
|
-
* Glob patterns to identify test files
|
|
176
|
-
*
|
|
177
|
-
* - `glob.
|
|
178
|
-
* - `glob.
|
|
179
|
-
* - `glob.
|
|
199
|
+
* Glob patterns to identify test files. Each field accepts a single pattern
|
|
200
|
+
* or an array of patterns; arrays are unioned during discovery.
|
|
201
|
+
* - `glob.test`: Glob pattern(s) for all test files (--glob.test)
|
|
202
|
+
* - `glob.browser`: Glob pattern(s) for the subset of browser test files (--glob.browser)
|
|
203
|
+
* - `glob.e2e`: Glob pattern(s) for the subset of e2e test files (--glob.e2e)
|
|
204
|
+
* - `glob.exclude`: Glob pattern(s) for paths to exclude from discovery (--glob.exclude)
|
|
180
205
|
*/
|
|
181
206
|
glob?: {
|
|
182
|
-
test?: string
|
|
183
|
-
browser?: string
|
|
184
|
-
e2e?: string
|
|
185
|
-
exclude?: string
|
|
207
|
+
test?: string | string[]
|
|
208
|
+
browser?: string | string[]
|
|
209
|
+
e2e?: string | string[]
|
|
210
|
+
exclude?: string | string[]
|
|
186
211
|
}
|
|
187
212
|
/** Max number of concurrent test workers (--concurrency) */
|
|
188
213
|
concurrency?: number | string
|
|
@@ -194,8 +219,8 @@ export interface RemixTestConfig {
|
|
|
194
219
|
| boolean
|
|
195
220
|
| {
|
|
196
221
|
dir?: string
|
|
197
|
-
include?: string[]
|
|
198
|
-
exclude?: string[]
|
|
222
|
+
include?: string | string[]
|
|
223
|
+
exclude?: string | string[]
|
|
199
224
|
statements?: number | string
|
|
200
225
|
lines?: number | string
|
|
201
226
|
branches?: number | string
|
|
@@ -211,12 +236,23 @@ export interface RemixTestConfig {
|
|
|
211
236
|
* PlaywrightTestConfig object. CLI `--playwrightConfig` only accepts a file path.
|
|
212
237
|
*/
|
|
213
238
|
playwrightConfig?: string | PlaywrightTestConfig
|
|
214
|
-
/**
|
|
215
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Pool used to run server and E2E test files. Forked child processes are the default,
|
|
241
|
+
* but worker threads are available for projects that prefer the previous behavior.
|
|
242
|
+
*/
|
|
243
|
+
pool?: RemixTestPool
|
|
244
|
+
/**
|
|
245
|
+
* Filter tests to specific playwright project(s) (--project). Accepts a single
|
|
246
|
+
* project name or an array of names; `--project` may be repeated on the CLI.
|
|
247
|
+
*/
|
|
248
|
+
project?: string | string[]
|
|
216
249
|
/** Test reporter (--reporter) */
|
|
217
250
|
reporter?: string
|
|
218
|
-
/**
|
|
219
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Test type(s) to run (--type). Accepts a single type or an array of types;
|
|
253
|
+
* `--type` may be repeated on the CLI. Valid values: "server", "browser", "e2e".
|
|
254
|
+
*/
|
|
255
|
+
type?: string | string[]
|
|
220
256
|
/** Watch mode — re-run tests on file changes (--watch) */
|
|
221
257
|
watch?: boolean
|
|
222
258
|
}
|
|
@@ -239,16 +275,17 @@ export interface ResolvedRemixTestConfig {
|
|
|
239
275
|
}
|
|
240
276
|
| undefined
|
|
241
277
|
glob: {
|
|
242
|
-
test: string
|
|
243
|
-
browser: string
|
|
244
|
-
e2e: string
|
|
245
|
-
exclude: string
|
|
278
|
+
test: string[]
|
|
279
|
+
browser: string[]
|
|
280
|
+
e2e: string[]
|
|
281
|
+
exclude: string[]
|
|
246
282
|
}
|
|
247
283
|
playwrightConfig: string | PlaywrightTestConfig | undefined
|
|
248
|
-
project: string | undefined
|
|
284
|
+
project: string[] | undefined
|
|
249
285
|
reporter: string
|
|
286
|
+
pool: RemixTestPool
|
|
250
287
|
setup: string | undefined
|
|
251
|
-
type: string
|
|
288
|
+
type: string[]
|
|
252
289
|
watch: boolean
|
|
253
290
|
}
|
|
254
291
|
|
|
@@ -259,12 +296,21 @@ export async function loadConfig(args: string[] = process.argv.slice(2), cwd = p
|
|
|
259
296
|
return config
|
|
260
297
|
}
|
|
261
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Returns the formatted `remix-test --help` text. Useful for embedding the
|
|
301
|
+
* runner's CLI options in higher-level tooling.
|
|
302
|
+
*
|
|
303
|
+
* @param _target Output stream the help text will be written to. Reserved
|
|
304
|
+
* for future use (e.g. width-aware formatting); currently
|
|
305
|
+
* unused.
|
|
306
|
+
* @returns The help text as a single string ready to write to a stream.
|
|
307
|
+
*/
|
|
262
308
|
export function getRemixTestHelpText(_target: NodeJS.WriteStream = process.stdout): string {
|
|
263
309
|
let lines = [
|
|
264
|
-
'Usage: remix-test [glob] [options]',
|
|
310
|
+
'Usage: remix-test [glob...] [options]',
|
|
265
311
|
'',
|
|
266
312
|
'Arguments:',
|
|
267
|
-
` glob Glob pattern for test files (default: "${defaultValues.glob.test}")`,
|
|
313
|
+
` glob Glob pattern(s) for test files (default: "${defaultValues.glob.test.join(', ')}")`,
|
|
268
314
|
'',
|
|
269
315
|
'Options:',
|
|
270
316
|
]
|
|
@@ -284,6 +330,19 @@ function parseCliArgs(args: string[]) {
|
|
|
284
330
|
return util.parseArgs({ args, options: cliOptions, allowPositionals: true })
|
|
285
331
|
}
|
|
286
332
|
|
|
333
|
+
function toArray<T>(value: T | readonly T[]): T[] {
|
|
334
|
+
return Array.isArray(value) ? [...value] : [value as T]
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function toCommaSeparatedArray(value: string | readonly string[]): string[] {
|
|
338
|
+
return toArray(value).flatMap((item) =>
|
|
339
|
+
item
|
|
340
|
+
.split(',')
|
|
341
|
+
.map((part) => part.trim())
|
|
342
|
+
.filter(Boolean),
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
287
346
|
function resolveConfig(
|
|
288
347
|
fileConfig: RemixTestConfig,
|
|
289
348
|
{ values: cliValues, positionals }: ReturnType<typeof parseCliArgs>,
|
|
@@ -291,14 +350,18 @@ function resolveConfig(
|
|
|
291
350
|
let fileCoverage = typeof fileConfig.coverage === 'boolean' ? {} : fileConfig.coverage || {}
|
|
292
351
|
return {
|
|
293
352
|
glob: {
|
|
294
|
-
test:
|
|
295
|
-
positionals
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
browser:
|
|
300
|
-
|
|
301
|
-
|
|
353
|
+
test: toArray(
|
|
354
|
+
positionals.length > 0
|
|
355
|
+
? positionals
|
|
356
|
+
: (cliValues['glob.test'] ?? fileConfig.glob?.test ?? defaultValues.glob.test),
|
|
357
|
+
),
|
|
358
|
+
browser: toArray(
|
|
359
|
+
cliValues['glob.browser'] ?? fileConfig.glob?.browser ?? defaultValues.glob.browser,
|
|
360
|
+
),
|
|
361
|
+
e2e: toArray(cliValues['glob.e2e'] ?? fileConfig.glob?.e2e ?? defaultValues.glob.e2e),
|
|
362
|
+
exclude: toArray(
|
|
363
|
+
cliValues['glob.exclude'] ?? fileConfig.glob?.exclude ?? defaultValues.glob.exclude,
|
|
364
|
+
),
|
|
302
365
|
},
|
|
303
366
|
browser: {
|
|
304
367
|
echo: cliValues['browser.echo'] ?? fileConfig.browser?.echo ?? defaultValues.browser.echo,
|
|
@@ -311,14 +374,20 @@ function resolveConfig(
|
|
|
311
374
|
cliValues.coverage === true || !!fileConfig.coverage
|
|
312
375
|
? {
|
|
313
376
|
dir: cliValues['coverage.dir'] ?? fileCoverage.dir ?? defaultValues.coverage!.dir,
|
|
314
|
-
include:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
377
|
+
include: (() => {
|
|
378
|
+
let raw =
|
|
379
|
+
cliValues['coverage.include'] ??
|
|
380
|
+
fileCoverage.include ??
|
|
381
|
+
defaultValues.coverage!.include
|
|
382
|
+
return raw === undefined ? undefined : toArray(raw)
|
|
383
|
+
})(),
|
|
384
|
+
exclude: (() => {
|
|
385
|
+
let raw =
|
|
386
|
+
cliValues['coverage.exclude'] ??
|
|
387
|
+
fileCoverage.exclude ??
|
|
388
|
+
defaultValues.coverage!.exclude
|
|
389
|
+
return raw === undefined ? undefined : toArray(raw)
|
|
390
|
+
})(),
|
|
322
391
|
statements:
|
|
323
392
|
cliValues['coverage.statements'] !== undefined
|
|
324
393
|
? Number(cliValues['coverage.statements'])
|
|
@@ -348,13 +417,25 @@ function resolveConfig(
|
|
|
348
417
|
setup: cliValues.setup ?? fileConfig.setup ?? defaultValues.setup,
|
|
349
418
|
playwrightConfig:
|
|
350
419
|
cliValues.playwrightConfig ?? fileConfig.playwrightConfig ?? defaultValues.playwrightConfig,
|
|
351
|
-
|
|
420
|
+
pool: resolvePool(cliValues.pool ?? fileConfig.pool ?? defaultValues.pool),
|
|
421
|
+
project: (() => {
|
|
422
|
+
let raw = cliValues.project ?? fileConfig.project ?? defaultValues.project
|
|
423
|
+
return raw === undefined ? undefined : toCommaSeparatedArray(raw)
|
|
424
|
+
})(),
|
|
352
425
|
reporter: cliValues.reporter ?? fileConfig.reporter ?? defaultValues.reporter,
|
|
353
|
-
type: cliValues.type ?? fileConfig.type ?? defaultValues.type,
|
|
426
|
+
type: toCommaSeparatedArray(cliValues.type ?? fileConfig.type ?? defaultValues.type),
|
|
354
427
|
watch: cliValues.watch ?? fileConfig.watch ?? defaultValues.watch,
|
|
355
428
|
}
|
|
356
429
|
}
|
|
357
430
|
|
|
431
|
+
function resolvePool(value: string): RemixTestPool {
|
|
432
|
+
if (value === 'forks' || value === 'threads') {
|
|
433
|
+
return value
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
throw new Error(`Unsupported test pool "${value}". Supported pools are: forks, threads`)
|
|
437
|
+
}
|
|
438
|
+
|
|
358
439
|
async function loadConfigFile(
|
|
359
440
|
configPath: string | undefined,
|
|
360
441
|
cwd: string,
|
|
@@ -366,11 +447,16 @@ async function loadConfigFile(
|
|
|
366
447
|
for (let candidate of candidates) {
|
|
367
448
|
try {
|
|
368
449
|
await fsp.access(candidate)
|
|
369
|
-
let mod = await importModule(candidate, import.meta)
|
|
370
|
-
return mod.default ?? mod
|
|
371
450
|
} catch {
|
|
372
|
-
// not found
|
|
451
|
+
// not found — try the next candidate
|
|
452
|
+
continue
|
|
373
453
|
}
|
|
454
|
+
// The file exists; let import errors propagate rather than silently
|
|
455
|
+
// falling through to defaults — that masking is what hid "Windows
|
|
456
|
+
// absolute paths aren't valid ESM specifiers" by classifying every
|
|
457
|
+
// browser test as a server test.
|
|
458
|
+
let mod = await importModule(candidate, import.meta)
|
|
459
|
+
return mod.default ?? mod
|
|
374
460
|
}
|
|
375
461
|
|
|
376
462
|
return {}
|
package/src/lib/context.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface TestServer {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Test Context providing utilities for testing via remix-test
|
|
18
|
+
* Test Context providing utilities for testing via `remix-test`. The context is
|
|
19
19
|
* passed as the first argument to the {@link test}/{@link it} functions.
|
|
20
20
|
*
|
|
21
21
|
* @example
|
|
@@ -36,8 +36,8 @@ export interface TestContext {
|
|
|
36
36
|
after(fn: () => void): void
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Mock tracker for the current test. Mirrors the shape of Node's
|
|
40
|
-
* `t.mock`. Method mocks created
|
|
39
|
+
* Mock tracker for the current test using {@link mock}. Mirrors the shape of Node's
|
|
40
|
+
* `t.mock`. Method mocks created via `t.mock` are auto-restored on test completion.
|
|
41
41
|
*/
|
|
42
42
|
mock: {
|
|
43
43
|
/**
|
|
@@ -77,8 +77,8 @@ export interface TestContext {
|
|
|
77
77
|
/**
|
|
78
78
|
* Wires a running test server up to a Playwright page so the test can drive
|
|
79
79
|
* it. The server is closed automatically when the test ends. Pair with
|
|
80
|
-
*
|
|
81
|
-
*
|
|
80
|
+
* {@link createTestServer} from `@remix-run/node-fetch-server/test` to spin
|
|
81
|
+
* up the server.
|
|
82
82
|
*
|
|
83
83
|
* @param server - The running server the page should target
|
|
84
84
|
* @returns A `Page` whose `baseURL` is set to `server.baseUrl`.
|
|
@@ -4,8 +4,8 @@ import { transformTypeScript } from './ts-transform.ts'
|
|
|
4
4
|
|
|
5
5
|
// Custom ESM loader hook for TypeScript files.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// Applies an un-minified esbuild transform that preserves line structure.
|
|
8
|
+
// This ensures V8 coverage byte offsets map
|
|
9
9
|
// cleanly to TypeScript source lines via the inline source map, giving
|
|
10
10
|
// accurate per-line coverage rather than collapsing multiple statements onto
|
|
11
11
|
// a single minified line.
|
package/src/lib/coverage.ts
CHANGED
|
@@ -236,7 +236,7 @@ export async function collectServerCoverageMap(
|
|
|
236
236
|
let { code } = await transformTypeScript(tsSource, filePath)
|
|
237
237
|
let success = await addV8EntryToCoverageMap(coverageMap, filePath, entry.functions, code)
|
|
238
238
|
if (success) converted++
|
|
239
|
-
} catch
|
|
239
|
+
} catch {
|
|
240
240
|
// Skip files that can't be converted
|
|
241
241
|
}
|
|
242
242
|
}
|
package/src/lib/fake-timers.ts
CHANGED
|
@@ -1,7 +1,46 @@
|
|
|
1
1
|
import { mock } from './mock.ts'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Handle returned by `mock.timers.enable()` for driving fake timers during a
|
|
5
|
+
* test. While enabled, `setTimeout`, `setInterval`, `clearTimeout`,
|
|
6
|
+
* `clearInterval`, and `Date.now` use the fake clock instead of the real one;
|
|
7
|
+
* timers fire only when the test calls `advance` (or `advanceAsync`).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* it('debounces save calls', (t) => {
|
|
12
|
+
* let timers = t.mock.timers.enable()
|
|
13
|
+
* let save = t.mock.fn()
|
|
14
|
+
* let debounced = debounce(save, 100)
|
|
15
|
+
* debounced(); debounced(); debounced()
|
|
16
|
+
* timers.advance(100)
|
|
17
|
+
* assert.equal(save.mock.calls.length, 1)
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
3
21
|
export interface FakeTimers {
|
|
22
|
+
/**
|
|
23
|
+
* Advance the fake clock by `ms` milliseconds, synchronously firing every
|
|
24
|
+
* timer whose deadline is reached during the advance.
|
|
25
|
+
*
|
|
26
|
+
* @param ms Number of milliseconds to advance.
|
|
27
|
+
*/
|
|
4
28
|
advance(ms: number): void
|
|
29
|
+
/**
|
|
30
|
+
* Like `advance`, but yields to microtasks between each timer firing so
|
|
31
|
+
* Promise continuations (and any timers they schedule) can settle before
|
|
32
|
+
* the next firing is processed. Use this when a callback awaits work that
|
|
33
|
+
* itself depends on the fake clock.
|
|
34
|
+
*
|
|
35
|
+
* @param ms Number of milliseconds to advance.
|
|
36
|
+
* @returns A promise that resolves once all reachable timers have fired.
|
|
37
|
+
*/
|
|
38
|
+
advanceAsync(ms: number): Promise<void>
|
|
39
|
+
/**
|
|
40
|
+
* Restore the original timer functions and the real clock. Called
|
|
41
|
+
* automatically after the test finishes; may also be called early to
|
|
42
|
+
* disable fake timers mid-test.
|
|
43
|
+
*/
|
|
5
44
|
restore(): void
|
|
6
45
|
}
|
|
7
46
|
|
|
@@ -35,20 +74,38 @@ export function createFakeTimers(): FakeTimers {
|
|
|
35
74
|
cancel as unknown as typeof clearInterval,
|
|
36
75
|
)
|
|
37
76
|
|
|
77
|
+
function takeNext(targetTime: number) {
|
|
78
|
+
let next = pending.filter((t) => t.time <= targetTime).sort((a, b) => a.time - b.time)[0]
|
|
79
|
+
if (!next) return null
|
|
80
|
+
currentTime = next.time
|
|
81
|
+
pending = pending.filter((t) => t.id !== next.id)
|
|
82
|
+
// Requeue intervals before running the callback so that calling
|
|
83
|
+
// clearInterval(id) from inside the callback can cancel the next firing.
|
|
84
|
+
if (next.repeatMs !== undefined) {
|
|
85
|
+
pending.push({ ...next, time: next.time + Math.max(1, next.repeatMs) })
|
|
86
|
+
}
|
|
87
|
+
return next
|
|
88
|
+
}
|
|
89
|
+
|
|
38
90
|
return {
|
|
39
91
|
advance(ms: number) {
|
|
40
92
|
let targetTime = currentTime + ms
|
|
41
93
|
while (true) {
|
|
42
|
-
let next =
|
|
94
|
+
let next = takeNext(targetTime)
|
|
95
|
+
if (!next) break
|
|
96
|
+
next.fn()
|
|
97
|
+
}
|
|
98
|
+
currentTime = targetTime
|
|
99
|
+
},
|
|
100
|
+
async advanceAsync(ms: number) {
|
|
101
|
+
let targetTime = currentTime + ms
|
|
102
|
+
while (true) {
|
|
103
|
+
let next = takeNext(targetTime)
|
|
43
104
|
if (!next) break
|
|
44
|
-
currentTime = next.time
|
|
45
|
-
pending = pending.filter((t) => t.id !== next.id)
|
|
46
|
-
// Requeue intervals before running the callback so that calling
|
|
47
|
-
// clearInterval(id) from inside the callback can cancel the next firing.
|
|
48
|
-
if (next.repeatMs !== undefined) {
|
|
49
|
-
pending.push({ ...next, time: next.time + Math.max(1, next.repeatMs) })
|
|
50
|
-
}
|
|
51
105
|
next.fn()
|
|
106
|
+
// Drain microtasks so Promise continuations (and any timers they
|
|
107
|
+
// schedule) can settle before we look for the next firing.
|
|
108
|
+
await Promise.resolve()
|
|
52
109
|
}
|
|
53
110
|
currentTime = targetTime
|
|
54
111
|
},
|
package/src/lib/framework.ts
CHANGED
|
@@ -57,6 +57,13 @@ function registerDescribe(
|
|
|
57
57
|
}
|
|
58
58
|
let suite: TestSuite = { name: fullName, tests: [], ...flags }
|
|
59
59
|
|
|
60
|
+
// Children inherit `skip`/`only` from their parent so that
|
|
61
|
+
// `describe.skip('parent', () => describe('child', () => it(...)))` actually
|
|
62
|
+
// skips the child's tests. The executor walks `rootSuites` as a flat list and
|
|
63
|
+
// only inspects each suite's own flag, so the propagation has to happen here.
|
|
64
|
+
if (currentSuite?.skip) suite.skip = true
|
|
65
|
+
if (currentSuite?.only) suite.only = true
|
|
66
|
+
|
|
60
67
|
// Inherit lifecycle hooks from parent suite (or root hooks if at top level)
|
|
61
68
|
let parent = currentSuite ?? rootHooks
|
|
62
69
|
if (parent.beforeEach) suite.beforeEach = parent.beforeEach
|
|
@@ -80,9 +87,20 @@ function registerDescribe(
|
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
// We implement this standalone so we can leverage multiple signatures through
|
|
91
|
+
// typedoc, but we need to do the `const describe = Object.assign()` thing below to
|
|
92
|
+
// get the modifiers onto the method in a typescript-aware way.
|
|
93
|
+
function describeImpl(name: string, fn: () => void): void
|
|
94
|
+
function describeImpl(name: string, meta: SuiteMeta, fn: () => void): void
|
|
95
|
+
function describeImpl(name: string, metaOrFn: SuiteMeta | (() => void), fn?: () => void): void {
|
|
96
|
+
let meta = typeof metaOrFn === 'function' ? {} : metaOrFn
|
|
97
|
+
let suiteFn = typeof metaOrFn === 'function' ? metaOrFn : fn!
|
|
98
|
+
registerDescribe(name, suiteFn, meta)
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
/**
|
|
84
|
-
* Groups related tests into a named suite. Suites can be nested
|
|
85
|
-
* as such
|
|
102
|
+
* Groups related tests into a named suite. Suites can be nested and will be displayed
|
|
103
|
+
* as such in reporter output. Lifecycle hooks registered inside
|
|
86
104
|
* a `describe` block apply only to tests within that block.
|
|
87
105
|
*
|
|
88
106
|
* @example
|
|
@@ -96,26 +114,20 @@ function registerDescribe(
|
|
|
96
114
|
* describe.todo('planned suite')
|
|
97
115
|
*
|
|
98
116
|
* @param name - The suite name shown in reporter output.
|
|
117
|
+
* @param meta - Suite metadata such as `skip` or `only`.
|
|
99
118
|
* @param fn - A function that registers the tests and lifecycle hooks in this suite.
|
|
100
119
|
*/
|
|
101
|
-
export const describe = Object.assign(
|
|
102
|
-
(name: string,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
todo: (name: string) => {
|
|
111
|
-
let fullName = currentSuite ? `${currentSuite.name} > ${name}` : name
|
|
112
|
-
if (rootSuites.some((s) => s.name === fullName)) {
|
|
113
|
-
throw new Error(`Duplicate suite name: "${fullName}"`)
|
|
114
|
-
}
|
|
115
|
-
rootSuites.push({ name: fullName, tests: [], todo: true })
|
|
116
|
-
},
|
|
120
|
+
export const describe = Object.assign(describeImpl, {
|
|
121
|
+
skip: (name: string, fn: () => void) => registerDescribe(name, fn, { skip: true }),
|
|
122
|
+
only: (name: string, fn: () => void) => registerDescribe(name, fn, { only: true }),
|
|
123
|
+
todo: (name: string) => {
|
|
124
|
+
let fullName = currentSuite ? `${currentSuite.name} > ${name}` : name
|
|
125
|
+
if (rootSuites.some((s) => s.name === fullName)) {
|
|
126
|
+
throw new Error(`Duplicate suite name: "${fullName}"`)
|
|
127
|
+
}
|
|
128
|
+
rootSuites.push({ name: fullName, tests: [], todo: true })
|
|
117
129
|
},
|
|
118
|
-
)
|
|
130
|
+
})
|
|
119
131
|
|
|
120
132
|
type SuiteMeta = { skip?: boolean; only?: boolean }
|
|
121
133
|
type TestMeta = { skip?: boolean; only?: boolean }
|
|
@@ -129,6 +141,17 @@ function registerIt(name: string, fn: TestFn, flags?: { only?: boolean; skip?: b
|
|
|
129
141
|
suite.tests.push({ name, fn, suite, ...flags })
|
|
130
142
|
}
|
|
131
143
|
|
|
144
|
+
// We implement this standalone so we can leverage multiple signatures through
|
|
145
|
+
// typedoc, but we need to do the `const it = Object.assign()` thing below to
|
|
146
|
+
// get the modifiers onto the method in a typescript-aware way.
|
|
147
|
+
function itImpl(name: string, fn: TestFn): void
|
|
148
|
+
function itImpl(name: string, meta: TestMeta, fn: TestFn): void
|
|
149
|
+
function itImpl(name: string, metaOrFn: TestMeta | TestFn, fn?: TestFn): void {
|
|
150
|
+
let meta = typeof metaOrFn === 'function' ? {} : metaOrFn
|
|
151
|
+
let testFn = typeof metaOrFn === 'function' ? metaOrFn : fn!
|
|
152
|
+
registerIt(name, testFn, meta)
|
|
153
|
+
}
|
|
154
|
+
|
|
132
155
|
/**
|
|
133
156
|
* Defines a single test case. The optional `TestContext` argument `t` provides
|
|
134
157
|
* mock helpers and per-test cleanup registration.
|
|
@@ -145,26 +168,20 @@ function registerIt(name: string, fn: TestFn, flags?: { only?: boolean; skip?: b
|
|
|
145
168
|
* it.todo('coming soon')
|
|
146
169
|
*
|
|
147
170
|
* @param name - The test name shown in reporter output.
|
|
171
|
+
* @param meta - Test metadata such as `skip` or `only`.
|
|
148
172
|
* @param fn - The test body, receiving a {@link TestContext} as its first argument.
|
|
149
173
|
*/
|
|
150
|
-
export const it = Object.assign(
|
|
151
|
-
(name: string,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
todo: (name: string) => {
|
|
160
|
-
let suite = currentSuite ?? getImplicitRootSuite()
|
|
161
|
-
if (suite.tests.some((t) => t.name === name)) {
|
|
162
|
-
throw new Error(`Duplicate test name: "${name}" in suite "${suite.name || 'Global'}"`)
|
|
163
|
-
}
|
|
164
|
-
suite.tests.push({ name, fn: () => {}, suite, todo: true })
|
|
165
|
-
},
|
|
174
|
+
export const it = Object.assign(itImpl, {
|
|
175
|
+
skip: (name: string, fn?: TestFn) => registerIt(name, fn ?? (() => {}), { skip: true }),
|
|
176
|
+
only: (name: string, fn: TestFn) => registerIt(name, fn, { only: true }),
|
|
177
|
+
todo: (name: string) => {
|
|
178
|
+
let suite = currentSuite ?? getImplicitRootSuite()
|
|
179
|
+
if (suite.tests.some((t) => t.name === name)) {
|
|
180
|
+
throw new Error(`Duplicate test name: "${name}" in suite "${suite.name || 'Global'}"`)
|
|
181
|
+
}
|
|
182
|
+
suite.tests.push({ name, fn: () => {}, suite, todo: true })
|
|
166
183
|
},
|
|
167
|
-
)
|
|
184
|
+
})
|
|
168
185
|
|
|
169
186
|
/** Alias for {@link describe}. */
|
|
170
187
|
export const suite = describe
|
package/src/lib/import-module.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
2
3
|
import { IS_BUN } from './runtime.ts'
|
|
3
4
|
|
|
4
5
|
interface ImportMetaWithResolve extends ImportMeta {
|
|
@@ -17,13 +18,23 @@ function hasImportMetaResolve(meta: ImportMeta): meta is ImportMetaWithResolve {
|
|
|
17
18
|
* @returns The imported module namespace.
|
|
18
19
|
*/
|
|
19
20
|
export async function importModule(specifier: string, meta: ImportMeta): Promise<any> {
|
|
21
|
+
// Absolute Windows paths (`C:\foo\bar.ts`) aren't valid ESM specifiers — only
|
|
22
|
+
// `file:///C:/foo/bar.ts` URLs, relative specifiers, or POSIX absolute paths
|
|
23
|
+
// are. Convert any absolute filesystem path to its `file:` URL so module
|
|
24
|
+
// loaders and `import()` accept it on every platform. POSIX absolute paths
|
|
25
|
+
// happen to work as specifiers without conversion, but going through
|
|
26
|
+
// `pathToFileURL` is safe and platform-agnostic.
|
|
27
|
+
let resolvedSpecifier = path.isAbsolute(specifier) ? pathToFileURL(specifier).href : specifier
|
|
28
|
+
|
|
20
29
|
if (IS_BUN) {
|
|
21
30
|
if (!hasImportMetaResolve(meta)) {
|
|
22
31
|
throw new Error('importModule() requires import.meta.resolve() in Bun')
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
return import(meta.resolve(
|
|
34
|
+
return import(meta.resolve(resolvedSpecifier, meta.url))
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
// node-tsx uses Node APIs that fail in Bun if statically imported
|
|
38
|
+
let { loadModule } = await import('@remix-run/node-tsx/load-module')
|
|
39
|
+
return loadModule(resolvedSpecifier, meta.url)
|
|
29
40
|
}
|
package/src/lib/reporters/dot.ts
CHANGED
|
@@ -6,10 +6,17 @@ import type { Counts, TestResult, TestResults } from './results.ts'
|
|
|
6
6
|
export class DotReporter implements Reporter {
|
|
7
7
|
#failures: { name: string; error: TestResult['error'] }[] = []
|
|
8
8
|
#dotCount = 0
|
|
9
|
+
#files = new Set<string>()
|
|
10
|
+
#suites = new Set<string>()
|
|
9
11
|
|
|
10
12
|
onSectionStart(_label: string) {}
|
|
11
13
|
|
|
12
14
|
onResult(results: TestResults, _env?: string) {
|
|
15
|
+
for (let test of results.tests) {
|
|
16
|
+
if (test.filePath) this.#files.add(test.filePath)
|
|
17
|
+
if (test.suiteName) this.#suites.add(test.suiteName)
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
for (let test of results.tests) {
|
|
14
21
|
if (test.status === 'passed') {
|
|
15
22
|
process.stdout.write(colors.green('.'))
|
|
@@ -47,6 +54,8 @@ export class DotReporter implements Reporter {
|
|
|
47
54
|
let { passed, failed, skipped, todo } = counts
|
|
48
55
|
let info = colors.cyan('ℹ')
|
|
49
56
|
console.log()
|
|
57
|
+
console.log(`${info} files ${this.#files.size}`)
|
|
58
|
+
console.log(`${info} suites ${this.#suites.size}`)
|
|
50
59
|
console.log(`${info} tests ${passed + failed + skipped + todo}`)
|
|
51
60
|
console.log(`${info} pass ${passed}`)
|
|
52
61
|
console.log(`${info} fail ${failed}`)
|