@remix-run/test 0.1.0 → 0.2.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.
Files changed (143) hide show
  1. package/README.md +140 -35
  2. package/dist/app/client/entry.d.ts +2 -0
  3. package/dist/app/client/entry.d.ts.map +1 -0
  4. package/dist/app/client/entry.js +324 -0
  5. package/dist/app/client/iframe.d.ts +2 -0
  6. package/dist/app/client/iframe.d.ts.map +1 -0
  7. package/dist/app/client/iframe.js +22 -0
  8. package/dist/app/server.d.ts +6 -0
  9. package/dist/app/server.d.ts.map +1 -0
  10. package/dist/app/server.js +303 -0
  11. package/dist/cli-entry.d.ts +3 -0
  12. package/dist/cli-entry.d.ts.map +1 -0
  13. package/dist/cli-entry.js +14 -0
  14. package/dist/cli.d.ts +7 -2
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +273 -139
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/lib/colors.d.ts +2 -0
  20. package/dist/lib/colors.d.ts.map +1 -0
  21. package/dist/lib/colors.js +2 -0
  22. package/dist/lib/config.d.ts +32 -1
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +125 -22
  25. package/dist/lib/context.d.ts +37 -13
  26. package/dist/lib/context.d.ts.map +1 -1
  27. package/dist/lib/context.js +19 -3
  28. package/dist/lib/coverage-loader.d.ts +16 -0
  29. package/dist/lib/coverage-loader.d.ts.map +1 -0
  30. package/dist/lib/coverage-loader.js +20 -0
  31. package/dist/lib/coverage.d.ts +28 -0
  32. package/dist/lib/coverage.d.ts.map +1 -0
  33. package/dist/lib/coverage.js +212 -0
  34. package/dist/lib/executor.d.ts +3 -26
  35. package/dist/lib/executor.d.ts.map +1 -1
  36. package/dist/lib/executor.js +11 -6
  37. package/dist/lib/fake-timers.d.ts +6 -0
  38. package/dist/lib/fake-timers.d.ts.map +1 -0
  39. package/dist/lib/fake-timers.js +45 -0
  40. package/dist/lib/import-module.d.ts +2 -0
  41. package/dist/lib/import-module.d.ts.map +1 -0
  42. package/dist/lib/import-module.js +29 -0
  43. package/dist/lib/normalize.d.ts +2 -0
  44. package/dist/lib/normalize.d.ts.map +1 -0
  45. package/dist/lib/{utils.js → normalize.js} +0 -9
  46. package/dist/lib/playwright.d.ts +1 -1
  47. package/dist/lib/playwright.d.ts.map +1 -1
  48. package/dist/lib/playwright.js +5 -8
  49. package/dist/lib/reporters/dot.d.ts +1 -2
  50. package/dist/lib/reporters/dot.d.ts.map +1 -1
  51. package/dist/lib/reporters/dot.js +2 -1
  52. package/dist/lib/reporters/files.d.ts +1 -2
  53. package/dist/lib/reporters/files.d.ts.map +1 -1
  54. package/dist/lib/reporters/files.js +2 -1
  55. package/dist/lib/reporters/index.d.ts +4 -5
  56. package/dist/lib/reporters/index.d.ts.map +1 -1
  57. package/dist/lib/reporters/index.js +3 -3
  58. package/dist/lib/reporters/results.d.ts +30 -0
  59. package/dist/lib/reporters/results.d.ts.map +1 -0
  60. package/dist/lib/reporters/results.js +1 -0
  61. package/dist/lib/reporters/spec.d.ts +1 -2
  62. package/dist/lib/reporters/spec.d.ts.map +1 -1
  63. package/dist/lib/reporters/spec.js +2 -1
  64. package/dist/lib/reporters/tap.d.ts +1 -2
  65. package/dist/lib/reporters/tap.d.ts.map +1 -1
  66. package/dist/lib/reporters/tap.js +1 -1
  67. package/dist/lib/runner-browser.d.ts +21 -0
  68. package/dist/lib/runner-browser.d.ts.map +1 -0
  69. package/dist/lib/runner-browser.js +117 -0
  70. package/dist/lib/runner.d.ts +7 -2
  71. package/dist/lib/runner.d.ts.map +1 -1
  72. package/dist/lib/runner.js +33 -4
  73. package/dist/lib/runtime.d.ts +2 -0
  74. package/dist/lib/runtime.d.ts.map +1 -0
  75. package/dist/lib/runtime.js +2 -0
  76. package/dist/lib/ts-transform.d.ts +4 -0
  77. package/dist/lib/ts-transform.d.ts.map +1 -0
  78. package/dist/lib/ts-transform.js +29 -0
  79. package/dist/lib/worker-e2e.js +5 -4
  80. package/dist/lib/worker.js +31 -3
  81. package/dist/test/coverage/fixture.d.ts +5 -0
  82. package/dist/test/coverage/fixture.d.ts.map +1 -0
  83. package/dist/test/coverage/fixture.js +32 -0
  84. package/dist/test/coverage/test-browser.d.ts +2 -0
  85. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  86. package/dist/test/coverage/test-browser.js +24 -0
  87. package/dist/test/coverage/test-e2e.d.ts +2 -0
  88. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  89. package/dist/test/coverage/test-e2e.js +60 -0
  90. package/dist/test/coverage/test-unit.d.ts +2 -0
  91. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  92. package/dist/test/coverage/test-unit.js +27 -0
  93. package/dist/test/framework.test.browser.d.ts +2 -0
  94. package/dist/test/framework.test.browser.d.ts.map +1 -0
  95. package/dist/test/framework.test.browser.js +107 -0
  96. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  97. package/dist/test/framework.test.e2e.js +34 -0
  98. package/package.json +30 -9
  99. package/src/app/client/entry.ts +353 -0
  100. package/src/app/client/iframe.ts +18 -0
  101. package/src/app/server.ts +336 -0
  102. package/src/cli-entry.ts +15 -0
  103. package/src/cli.ts +322 -148
  104. package/src/index.ts +1 -0
  105. package/src/lib/colors.ts +3 -0
  106. package/src/lib/config.ts +169 -23
  107. package/src/lib/context.ts +59 -17
  108. package/src/lib/coverage-loader.ts +31 -0
  109. package/src/lib/coverage.ts +320 -0
  110. package/src/lib/executor.ts +18 -35
  111. package/src/lib/fake-timers.ts +64 -0
  112. package/src/lib/import-module.ts +29 -0
  113. package/src/lib/{utils.ts → normalize.ts} +0 -18
  114. package/src/lib/playwright.ts +5 -7
  115. package/src/lib/reporters/dot.ts +3 -2
  116. package/src/lib/reporters/files.ts +3 -2
  117. package/src/lib/reporters/index.ts +4 -5
  118. package/src/lib/reporters/results.ts +29 -0
  119. package/src/lib/reporters/spec.ts +3 -2
  120. package/src/lib/reporters/tap.ts +2 -2
  121. package/src/lib/runner-browser.ts +165 -0
  122. package/src/lib/runner.ts +62 -10
  123. package/src/lib/runtime.ts +2 -0
  124. package/src/lib/ts-transform.ts +36 -0
  125. package/src/lib/worker-e2e.ts +7 -5
  126. package/src/lib/worker.ts +24 -4
  127. package/src/test/coverage/fixture.ts +34 -0
  128. package/src/test/coverage/test-browser.ts +29 -0
  129. package/src/test/coverage/test-e2e.ts +70 -0
  130. package/src/test/coverage/test-unit.ts +32 -0
  131. package/tsconfig.json +3 -1
  132. package/dist/lib/e2e-server.d.ts +0 -11
  133. package/dist/lib/e2e-server.d.ts.map +0 -1
  134. package/dist/lib/e2e-server.js +0 -15
  135. package/dist/lib/framework.test.d.ts +0 -2
  136. package/dist/lib/framework.test.d.ts.map +0 -1
  137. package/dist/lib/framework.test.e2e.d.ts.map +0 -1
  138. package/dist/lib/framework.test.e2e.js +0 -29
  139. package/dist/lib/framework.test.js +0 -283
  140. package/dist/lib/utils.d.ts +0 -16
  141. package/dist/lib/utils.d.ts.map +0 -1
  142. package/src/lib/e2e-server.ts +0 -28
  143. /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
package/src/lib/config.ts CHANGED
@@ -1,9 +1,34 @@
1
+ import * as fsp from 'node:fs/promises'
1
2
  import * as os from 'node:os'
2
3
  import * as path from 'node:path'
3
- import * as fsp from 'node:fs/promises'
4
+ import { fileURLToPath } from 'node:url'
4
5
  import * as util from 'node:util'
5
- import { tsImport } from 'tsx/esm/api'
6
6
  import type { PlaywrightTestConfig } from 'playwright/test'
7
+ import { importModule } from './import-module.ts'
8
+
9
+ export const IS_RUNNING_FROM_SRC = path.extname(new URL(import.meta.url).pathname) === '.ts'
10
+
11
+ /*
12
+ * The root directory for the test code. Coverage URLs are emitted as
13
+ * `/scripts/<rel-from-rootDir>` and resolved back via the same anchor.
14
+ *
15
+ * - In a published install: `process.cwd()`, since deps and user source all
16
+ * live under it.
17
+ * - In monorepo src mode: the monorepo root, computed by walking back from
18
+ * the resolved `@remix-run/test` source path. `process.cwd()` doesn't work
19
+ * here because workspace deps and node_modules live above the per-package
20
+ * cwd.
21
+ */
22
+ export function getBrowserTestRootDir(): string {
23
+ return IS_RUNNING_FROM_SRC
24
+ ? // Resolve to packages/test/src/index.ts and the pop 3 directories off to the repo root
25
+ path
26
+ .dirname(fileURLToPath(import.meta.resolve('@remix-run/test')))
27
+ .split(path.sep)
28
+ .slice(0, -3)
29
+ .join(path.sep)
30
+ : process.cwd()
31
+ }
7
32
 
8
33
  // prettier-ignore
9
34
  // Note: `description` is not a field used by parseArgs(), it's an additional field
@@ -17,10 +42,18 @@ const cliOptions = {
17
42
  type: 'boolean',
18
43
  description: 'Open browser window and keep open after tests finish',
19
44
  },
45
+ 'glob.browser': {
46
+ type: 'string',
47
+ description: 'Glob pattern for browser test files',
48
+ },
20
49
  'glob.e2e': {
21
50
  type: 'string',
22
51
  description: 'Glob pattern for E2E test files',
23
52
  },
53
+ 'glob.exclude': {
54
+ type: 'string',
55
+ description: 'Glob pattern for paths to exclude from discovery',
56
+ },
24
57
  'glob.test': {
25
58
  type: 'string',
26
59
  description: 'Glob pattern for all test files',
@@ -34,6 +67,40 @@ const cliOptions = {
34
67
  type: 'string',
35
68
  description: 'Path to config file (default: remix-test.config.ts)',
36
69
  },
70
+ coverage: {
71
+ type: 'boolean',
72
+ description: 'Enable or disable coverage collection (default: false)',
73
+ },
74
+ 'coverage.dir': {
75
+ type: 'string',
76
+ description: 'Directory to output coverage reports (default: .coverage)',
77
+ },
78
+ 'coverage.include': {
79
+ type: 'string',
80
+ multiple: true,
81
+ description: 'Glob pattern(s) for files to include in coverage',
82
+ },
83
+ 'coverage.exclude': {
84
+ type: 'string',
85
+ multiple: true,
86
+ description: 'Glob pattern(s) for files to exclude from coverage',
87
+ },
88
+ 'coverage.branches': {
89
+ type: 'string',
90
+ description: 'Branches coverage threshold percentage',
91
+ },
92
+ 'coverage.functions': {
93
+ type: 'string',
94
+ description: 'Functions coverage threshold percentage',
95
+ },
96
+ 'coverage.lines': {
97
+ type: 'string',
98
+ description: 'Lines coverage threshold percentage',
99
+ },
100
+ 'coverage.statements': {
101
+ type: 'string',
102
+ description: 'Statements coverage threshold percentage',
103
+ },
37
104
  setup: {
38
105
  type: 'string',
39
106
  short: 's',
@@ -56,7 +123,7 @@ const cliOptions = {
56
123
  type: {
57
124
  type: 'string',
58
125
  short: 't',
59
- description: 'Comma-separated test types to run (default: server,e2e)',
126
+ description: 'Comma-separated test types to run (default: server,browser,e2e)',
60
127
  },
61
128
  watch: {
62
129
  type: 'boolean',
@@ -71,12 +138,23 @@ const defaultValues: ResolvedRemixTestConfig = {
71
138
  open: false,
72
139
  },
73
140
  concurrency: os.availableParallelism(),
141
+ coverage: {
142
+ dir: '.coverage',
143
+ include: undefined,
144
+ exclude: undefined,
145
+ statements: undefined,
146
+ lines: undefined,
147
+ branches: undefined,
148
+ functions: undefined,
149
+ },
74
150
  glob: {
75
- test: '**/*.test?(.e2e).{ts,tsx}',
151
+ test: '**/*.test{,.e2e,.browser}.{ts,tsx}',
152
+ browser: '**/*.test.browser.{ts,tsx}',
76
153
  e2e: '**/*.test.e2e.{ts,tsx}',
154
+ exclude: 'node_modules/**',
77
155
  },
78
- reporter: process.env.CI === 'true' ? 'dot' : 'spec',
79
- type: 'server,e2e',
156
+ reporter: process.env.CI === 'true' ? 'files' : 'spec',
157
+ type: 'server,browser,e2e',
80
158
  setup: undefined,
81
159
  playwrightConfig: undefined,
82
160
  project: undefined,
@@ -96,14 +174,33 @@ export interface RemixTestConfig {
96
174
  /**
97
175
  * Glob patterns to identify test files
98
176
  * - `glob.test`: Glob pattern for all test files (--glob.test)
177
+ * - `glob.browser`: Glob pattern for the subset of browser test files (--glob.browser)
99
178
  * - `glob.e2e`: Glob pattern for the subset of e2e test files (--glob.e2e)
179
+ * - `glob.exclude`: Glob pattern for paths to exclude from discovery (--glob.exclude)
100
180
  */
101
181
  glob?: {
102
182
  test?: string
183
+ browser?: string
103
184
  e2e?: string
185
+ exclude?: string
104
186
  }
105
187
  /** Max number of concurrent test workers (--concurrency) */
106
188
  concurrency?: number | string
189
+ /**
190
+ * Coverage configuration. `true` enables with defaults; an object enables with settings;
191
+ * `false` disables. CLI `--coverage` flag overrides the boolean aspect.
192
+ */
193
+ coverage?:
194
+ | boolean
195
+ | {
196
+ dir?: string
197
+ include?: string[]
198
+ exclude?: string[]
199
+ statements?: number | string
200
+ lines?: number | string
201
+ branches?: number | string
202
+ functions?: number | string
203
+ }
107
204
  /**
108
205
  * Path to a module that exports `globalSetup` and/or `globalTeardown` functions,
109
206
  * called once before and after the test run respectively. (--setup)
@@ -130,9 +227,22 @@ export interface ResolvedRemixTestConfig {
130
227
  open?: boolean
131
228
  }
132
229
  concurrency: number
230
+ coverage:
231
+ | {
232
+ dir: string
233
+ include?: string[]
234
+ exclude?: string[]
235
+ statements?: number
236
+ lines?: number
237
+ branches?: number
238
+ functions?: number
239
+ }
240
+ | undefined
133
241
  glob: {
134
242
  test: string
243
+ browser: string
135
244
  e2e: string
245
+ exclude: string
136
246
  }
137
247
  playwrightConfig: string | PlaywrightTestConfig | undefined
138
248
  project: string | undefined
@@ -142,19 +252,14 @@ export interface ResolvedRemixTestConfig {
142
252
  watch: boolean
143
253
  }
144
254
 
145
- export async function loadConfig() {
146
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
147
- console.log(generateHelp())
148
- process.exit(0)
149
- }
150
-
151
- let parsed = parseCliArgs()
152
- let fileConfig = await loadConfigFile(parsed.values.config)
255
+ export async function loadConfig(args: string[] = process.argv.slice(2), cwd = process.cwd()) {
256
+ let parsed = parseCliArgs(args)
257
+ let fileConfig = await loadConfigFile(parsed.values.config, cwd)
153
258
  let config = resolveConfig(fileConfig, parsed)
154
259
  return config
155
260
  }
156
261
 
157
- function generateHelp(): string {
262
+ export function getRemixTestHelpText(_target: NodeJS.WriteStream = process.stdout): string {
158
263
  let lines = [
159
264
  'Usage: remix-test [glob] [options]',
160
265
  '',
@@ -175,7 +280,7 @@ function generateHelp(): string {
175
280
  return lines.join('\n')
176
281
  }
177
282
 
178
- function parseCliArgs(args = process.argv.slice(2)) {
283
+ function parseCliArgs(args: string[]) {
179
284
  return util.parseArgs({ args, options: cliOptions, allowPositionals: true })
180
285
  }
181
286
 
@@ -183,6 +288,7 @@ function resolveConfig(
183
288
  fileConfig: RemixTestConfig,
184
289
  { values: cliValues, positionals }: ReturnType<typeof parseCliArgs>,
185
290
  ): ResolvedRemixTestConfig {
291
+ let fileCoverage = typeof fileConfig.coverage === 'boolean' ? {} : fileConfig.coverage || {}
186
292
  return {
187
293
  glob: {
188
294
  test:
@@ -190,7 +296,9 @@ function resolveConfig(
190
296
  cliValues['glob.test'] ??
191
297
  fileConfig.glob?.test ??
192
298
  defaultValues.glob.test,
299
+ browser: cliValues['glob.browser'] ?? fileConfig.glob?.browser ?? defaultValues.glob.browser,
193
300
  e2e: cliValues['glob.e2e'] ?? fileConfig.glob?.e2e ?? defaultValues.glob.e2e,
301
+ exclude: cliValues['glob.exclude'] ?? fileConfig.glob?.exclude ?? defaultValues.glob.exclude,
194
302
  },
195
303
  browser: {
196
304
  echo: cliValues['browser.echo'] ?? fileConfig.browser?.echo ?? defaultValues.browser.echo,
@@ -199,6 +307,44 @@ function resolveConfig(
199
307
  concurrency: Number(
200
308
  cliValues.concurrency ?? fileConfig.concurrency ?? defaultValues.concurrency,
201
309
  ),
310
+ coverage:
311
+ cliValues.coverage === true || !!fileConfig.coverage
312
+ ? {
313
+ dir: cliValues['coverage.dir'] ?? fileCoverage.dir ?? defaultValues.coverage!.dir,
314
+ include:
315
+ cliValues['coverage.include'] ??
316
+ fileCoverage.include ??
317
+ defaultValues.coverage!.include,
318
+ exclude:
319
+ cliValues['coverage.exclude'] ??
320
+ fileCoverage.exclude ??
321
+ defaultValues.coverage!.exclude,
322
+ statements:
323
+ cliValues['coverage.statements'] !== undefined
324
+ ? Number(cliValues['coverage.statements'])
325
+ : fileCoverage.statements !== undefined
326
+ ? Number(fileCoverage.statements)
327
+ : undefined,
328
+ lines:
329
+ cliValues['coverage.lines'] !== undefined
330
+ ? Number(cliValues['coverage.lines'])
331
+ : fileCoverage.lines !== undefined
332
+ ? Number(fileCoverage.lines)
333
+ : undefined,
334
+ branches:
335
+ cliValues['coverage.branches'] !== undefined
336
+ ? Number(cliValues['coverage.branches'])
337
+ : fileCoverage.branches !== undefined
338
+ ? Number(fileCoverage.branches)
339
+ : undefined,
340
+ functions:
341
+ cliValues['coverage.functions'] !== undefined
342
+ ? Number(cliValues['coverage.functions'])
343
+ : fileCoverage.functions !== undefined
344
+ ? Number(fileCoverage.functions)
345
+ : undefined,
346
+ }
347
+ : undefined,
202
348
  setup: cliValues.setup ?? fileConfig.setup ?? defaultValues.setup,
203
349
  playwrightConfig:
204
350
  cliValues.playwrightConfig ?? fileConfig.playwrightConfig ?? defaultValues.playwrightConfig,
@@ -209,18 +355,18 @@ function resolveConfig(
209
355
  }
210
356
  }
211
357
 
212
- async function loadConfigFile(configPath?: string): Promise<RemixTestConfig> {
358
+ async function loadConfigFile(
359
+ configPath: string | undefined,
360
+ cwd: string,
361
+ ): Promise<RemixTestConfig> {
213
362
  let candidates = configPath
214
- ? [path.resolve(process.cwd(), configPath)]
215
- : [
216
- path.join(process.cwd(), 'remix-test.config.ts'),
217
- path.join(process.cwd(), 'remix-test.config.js'),
218
- ]
363
+ ? [path.resolve(cwd, configPath)]
364
+ : [path.join(cwd, 'remix-test.config.ts'), path.join(cwd, 'remix-test.config.js')]
219
365
 
220
366
  for (let candidate of candidates) {
221
367
  try {
222
368
  await fsp.access(candidate)
223
- let mod = await tsImport(candidate, { parentURL: import.meta.url })
369
+ let mod = await importModule(candidate, import.meta)
224
370
  return mod.default ?? mod
225
371
  } catch {
226
372
  // not found or failed to load — try next
@@ -1,9 +1,19 @@
1
1
  import type { Browser, Page } from 'playwright'
2
- import { mock, type MockFunction, type MockCall, type MockContext } from './mock.ts'
3
-
4
- import type { CreateServerFunction } from './e2e-server.ts'
2
+ import type { V8CoverageEntry } from './coverage.ts'
3
+ import { createFakeTimers, type FakeTimers } from './fake-timers.ts'
4
+ import { mock, type MockCall, type MockContext, type MockFunction } from './mock.ts'
5
5
  import type { getPlaywrightPageOptions } from './playwright.ts'
6
6
 
7
+ /**
8
+ * The shape `t.serve()` consumes. Matches the result of `createTestServer`
9
+ * from `@remix-run/node-fetch-server/test`, but any object with a `baseUrl`
10
+ * and async `close()` works.
11
+ */
12
+ export interface TestServer {
13
+ baseUrl: string
14
+ close(): Promise<void>
15
+ }
16
+
7
17
  /**
8
18
  * Test Context providing utilities for testing via remix-test. The context is
9
19
  * passed as the first argument to the {@link test}/{@link it} functions.
@@ -58,20 +68,36 @@ export interface TestContext {
58
68
  }
59
69
 
60
70
  /**
61
- * Starts a test server with the provided request handler.
71
+ * Activates fake timers for testing time-dependent code.
72
+ *
73
+ * @returns {FakeTimers} A fake timers instance for controlling time
74
+ */
75
+ useFakeTimers(): FakeTimers
76
+
77
+ /**
78
+ * Wires a running test server up to a Playwright page so the test can drive
79
+ * it. The server is closed automatically when the test ends. Pair with
80
+ * `createTestServer` from `@remix-run/node-fetch-server/test` (or any other
81
+ * source of a `{ baseUrl, close }` handle) to spin up the server first.
62
82
  *
63
- * @param {(req: Request) => Promise<Response>} handler - Function handling incoming requests
64
- * @returns {Promise<Page>} A promise resolving to a page instance for the server
83
+ * @param server - The running server the page should target
84
+ * @returns A `Page` whose `baseURL` is set to `server.baseUrl`.
65
85
  */
66
- serve(handler: (req: Request) => Promise<Response>): Promise<Page>
86
+ serve(server: TestServer): Promise<Page>
87
+ }
88
+
89
+ export interface CreateTestContextOptions {
90
+ addE2ECoverageEntries: (value: { entries: V8CoverageEntry[]; baseUrl: string }) => void
91
+ browser: Browser
92
+ coverage: boolean
93
+ open: boolean
94
+ playwrightPageOptions: ReturnType<typeof getPlaywrightPageOptions>
67
95
  }
68
96
 
69
- export function createTestContext(options: {
70
- createServer?: CreateServerFunction
71
- browser?: Browser
72
- open?: boolean
73
- playwrightPageOptions?: ReturnType<typeof getPlaywrightPageOptions>
74
- }): { testContext: TestContext; cleanup(): Promise<void> } {
97
+ export function createTestContext(options?: CreateTestContextOptions): {
98
+ testContext: TestContext
99
+ cleanup(): Promise<void>
100
+ } {
75
101
  let cleanups: Array<() => void | Promise<void>> = []
76
102
 
77
103
  let testContext: TestContext = {
@@ -86,12 +112,16 @@ export function createTestContext(options: {
86
112
  after(fn) {
87
113
  cleanups.push(fn)
88
114
  },
89
- async serve(handler) {
90
- if (!options.createServer || !options.browser) {
115
+ useFakeTimers() {
116
+ let timers = createFakeTimers()
117
+ cleanups.push(timers.restore)
118
+ return timers
119
+ },
120
+ async serve(server) {
121
+ if (!options || !options.browser) {
91
122
  throw new Error('t.serve() is only available in E2E test suites')
92
123
  }
93
124
 
94
- let server = await options.createServer(handler)
95
125
  let page = await options.browser.newPage({
96
126
  ...options.playwrightPageOptions,
97
127
  baseURL: server.baseUrl,
@@ -103,6 +133,18 @@ export function createTestContext(options: {
103
133
  page.setDefaultTimeout(options.playwrightPageOptions.actionTimeout)
104
134
  }
105
135
 
136
+ let coverageEnabled = options.coverage && options.browser.browserType().name() === 'chromium'
137
+ if (coverageEnabled) {
138
+ await page.coverage.startJSCoverage({ resetOnNavigation: false })
139
+ cleanups.push(async () => {
140
+ let entries = await page.coverage.stopJSCoverage()
141
+ options.addE2ECoverageEntries?.({
142
+ entries: entries as unknown as V8CoverageEntry[],
143
+ baseUrl: server.baseUrl,
144
+ })
145
+ })
146
+ }
147
+
106
148
  cleanups.push(async () => {
107
149
  if (!options.open) {
108
150
  await page.close()
@@ -123,4 +165,4 @@ export function createTestContext(options: {
123
165
  }
124
166
  }
125
167
 
126
- export type { MockFunction, MockCall, MockContext }
168
+ export type { MockCall, MockContext, MockFunction }
@@ -0,0 +1,31 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { transformTypeScript } from './ts-transform.ts'
4
+
5
+ // Custom ESM loader hook for TypeScript files.
6
+ //
7
+ // Replaces tsx's minified transformation with an un-minified esbuild transform
8
+ // that preserves line structure. This ensures V8 coverage byte offsets map
9
+ // cleanly to TypeScript source lines via the inline source map, giving
10
+ // accurate per-line coverage rather than collapsing multiple statements onto
11
+ // a single minified line.
12
+
13
+ export async function load(
14
+ url: string,
15
+ context: { format?: string },
16
+ nextLoad: (
17
+ url: string,
18
+ context: { format?: string },
19
+ ) => Promise<{ format: string; source: string }>,
20
+ ) {
21
+ let cleanUrl = url.includes('?') ? url.slice(0, url.indexOf('?')) : url
22
+ if (!cleanUrl.endsWith('.ts') && !cleanUrl.endsWith('.tsx')) {
23
+ return nextLoad(url, context)
24
+ }
25
+
26
+ let filePath = fileURLToPath(cleanUrl)
27
+ let source = await readFile(filePath, 'utf-8')
28
+ let { code } = await transformTypeScript(source, filePath)
29
+
30
+ return { format: 'module', source: code, shortCircuit: true }
31
+ }