@remix-run/test 0.1.0 → 0.3.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 (159) hide show
  1. package/README.md +161 -50
  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 +328 -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 +319 -140
  17. package/dist/index.d.ts +2 -1
  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 +59 -14
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +181 -38
  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 +13 -0
  38. package/dist/lib/fake-timers.d.ts.map +1 -0
  39. package/dist/lib/fake-timers.js +64 -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 +38 -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 +12 -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 +12 -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 +12 -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 +11 -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 +123 -0
  70. package/dist/lib/runner.d.ts +24 -2
  71. package/dist/lib/runner.d.ts.map +1 -1
  72. package/dist/lib/runner.js +216 -38
  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-file.d.ts +11 -0
  80. package/dist/lib/worker-e2e-file.d.ts.map +1 -0
  81. package/dist/lib/worker-e2e-file.js +69 -0
  82. package/dist/lib/worker-e2e.js +11 -46
  83. package/dist/lib/worker-process.d.ts +2 -0
  84. package/dist/lib/worker-process.d.ts.map +1 -0
  85. package/dist/lib/worker-process.js +55 -0
  86. package/dist/lib/worker-results.d.ts +3 -0
  87. package/dist/lib/worker-results.d.ts.map +1 -0
  88. package/dist/lib/worker-results.js +20 -0
  89. package/dist/lib/worker-server.d.ts +10 -0
  90. package/dist/lib/worker-server.d.ts.map +1 -0
  91. package/dist/lib/worker-server.js +113 -0
  92. package/dist/lib/worker.js +7 -28
  93. package/dist/test/coverage/fixture.d.ts +5 -0
  94. package/dist/test/coverage/fixture.d.ts.map +1 -0
  95. package/dist/test/coverage/fixture.js +32 -0
  96. package/dist/test/coverage/test-browser.d.ts +2 -0
  97. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  98. package/dist/test/coverage/test-browser.js +24 -0
  99. package/dist/test/coverage/test-e2e.d.ts +2 -0
  100. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  101. package/dist/test/coverage/test-e2e.js +60 -0
  102. package/dist/test/coverage/test-unit.d.ts +2 -0
  103. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  104. package/dist/test/coverage/test-unit.js +27 -0
  105. package/dist/test/framework.test.browser.d.ts +2 -0
  106. package/dist/test/framework.test.browser.d.ts.map +1 -0
  107. package/dist/test/framework.test.browser.js +107 -0
  108. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  109. package/dist/test/framework.test.e2e.js +34 -0
  110. package/package.json +30 -9
  111. package/src/app/client/entry.ts +357 -0
  112. package/src/app/client/iframe.ts +18 -0
  113. package/src/app/server.ts +336 -0
  114. package/src/cli-entry.ts +15 -0
  115. package/src/cli.ts +382 -145
  116. package/src/index.ts +2 -1
  117. package/src/lib/colors.ts +3 -0
  118. package/src/lib/config.ts +266 -54
  119. package/src/lib/context.ts +59 -17
  120. package/src/lib/coverage-loader.ts +31 -0
  121. package/src/lib/coverage.ts +320 -0
  122. package/src/lib/executor.ts +18 -35
  123. package/src/lib/fake-timers.ts +89 -0
  124. package/src/lib/import-module.ts +39 -0
  125. package/src/lib/{utils.ts → normalize.ts} +0 -18
  126. package/src/lib/playwright.ts +5 -7
  127. package/src/lib/reporters/dot.ts +12 -2
  128. package/src/lib/reporters/files.ts +12 -2
  129. package/src/lib/reporters/index.ts +4 -5
  130. package/src/lib/reporters/results.ts +29 -0
  131. package/src/lib/reporters/spec.ts +12 -2
  132. package/src/lib/reporters/tap.ts +11 -2
  133. package/src/lib/runner-browser.ts +171 -0
  134. package/src/lib/runner.ts +308 -53
  135. package/src/lib/runtime.ts +2 -0
  136. package/src/lib/ts-transform.ts +36 -0
  137. package/src/lib/worker-e2e-file.ts +98 -0
  138. package/src/lib/worker-e2e.ts +14 -49
  139. package/src/lib/worker-process.ts +69 -0
  140. package/src/lib/worker-results.ts +22 -0
  141. package/src/lib/worker-server.ts +123 -0
  142. package/src/lib/worker.ts +8 -28
  143. package/src/test/coverage/fixture.ts +34 -0
  144. package/src/test/coverage/test-browser.ts +29 -0
  145. package/src/test/coverage/test-e2e.ts +70 -0
  146. package/src/test/coverage/test-unit.ts +32 -0
  147. package/tsconfig.json +3 -1
  148. package/dist/lib/e2e-server.d.ts +0 -11
  149. package/dist/lib/e2e-server.d.ts.map +0 -1
  150. package/dist/lib/e2e-server.js +0 -15
  151. package/dist/lib/framework.test.d.ts +0 -2
  152. package/dist/lib/framework.test.d.ts.map +0 -1
  153. package/dist/lib/framework.test.e2e.d.ts.map +0 -1
  154. package/dist/lib/framework.test.e2e.js +0 -29
  155. package/dist/lib/framework.test.js +0 -283
  156. package/dist/lib/utils.d.ts +0 -16
  157. package/dist/lib/utils.d.ts.map +0 -1
  158. package/src/lib/e2e-server.ts +0 -28
  159. /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
@@ -1,8 +1,31 @@
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
+ import { importModule } from "./import-module.js";
7
+ export const IS_RUNNING_FROM_SRC = path.extname(new URL(import.meta.url).pathname) === '.ts';
8
+ /*
9
+ * The root directory for the test code. Coverage URLs are emitted as
10
+ * `/scripts/<rel-from-rootDir>` and resolved back via the same anchor.
11
+ *
12
+ * - In a published install: `process.cwd()`, since deps and user source all
13
+ * live under it.
14
+ * - In monorepo src mode: the monorepo root, computed by walking back from
15
+ * the resolved `@remix-run/test` source path. `process.cwd()` doesn't work
16
+ * here because workspace deps and node_modules live above the per-package
17
+ * cwd.
18
+ */
19
+ export function getBrowserTestRootDir() {
20
+ return IS_RUNNING_FROM_SRC
21
+ ? // Resolve to packages/test/src/index.ts and the pop 3 directories off to the repo root
22
+ path
23
+ .dirname(fileURLToPath(import.meta.resolve('@remix-run/test')))
24
+ .split(path.sep)
25
+ .slice(0, -3)
26
+ .join(path.sep)
27
+ : process.cwd();
28
+ }
6
29
  // prettier-ignore
7
30
  // Note: `description` is not a field used by parseArgs(), it's an additional field
8
31
  // we use for `--help`
@@ -15,13 +38,25 @@ const cliOptions = {
15
38
  type: 'boolean',
16
39
  description: 'Open browser window and keep open after tests finish',
17
40
  },
41
+ 'glob.browser': {
42
+ type: 'string',
43
+ multiple: true,
44
+ description: 'Glob pattern(s) for browser test files',
45
+ },
18
46
  'glob.e2e': {
19
47
  type: 'string',
20
- description: 'Glob pattern for E2E test files',
48
+ multiple: true,
49
+ description: 'Glob pattern(s) for E2E test files',
50
+ },
51
+ 'glob.exclude': {
52
+ type: 'string',
53
+ multiple: true,
54
+ description: 'Glob pattern(s) for paths to exclude from discovery',
21
55
  },
22
56
  'glob.test': {
23
57
  type: 'string',
24
- description: 'Glob pattern for all test files',
58
+ multiple: true,
59
+ description: 'Glob pattern(s) for all test files',
25
60
  },
26
61
  concurrency: {
27
62
  type: 'string',
@@ -32,6 +67,40 @@ const cliOptions = {
32
67
  type: 'string',
33
68
  description: 'Path to config file (default: remix-test.config.ts)',
34
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
+ },
35
104
  setup: {
36
105
  type: 'string',
37
106
  short: 's',
@@ -44,7 +113,12 @@ const cliOptions = {
44
113
  project: {
45
114
  type: 'string',
46
115
  short: 'p',
47
- description: 'Filter to a specific Playwright project (comma-separated)',
116
+ multiple: true,
117
+ description: 'Filter to specific Playwright project(s)',
118
+ },
119
+ pool: {
120
+ type: 'string',
121
+ description: 'Pool used to run server and E2E test files: forks, threads (default: forks)',
48
122
  },
49
123
  reporter: {
50
124
  type: 'string',
@@ -54,7 +128,8 @@ const cliOptions = {
54
128
  type: {
55
129
  type: 'string',
56
130
  short: 't',
57
- description: 'Comma-separated test types to run (default: server,e2e)',
131
+ multiple: true,
132
+ description: 'Test types to run (default: server, browser, e2e)',
58
133
  },
59
134
  watch: {
60
135
  type: 'boolean',
@@ -68,33 +143,41 @@ const defaultValues = {
68
143
  open: false,
69
144
  },
70
145
  concurrency: os.availableParallelism(),
146
+ coverage: {
147
+ dir: '.coverage',
148
+ include: undefined,
149
+ exclude: undefined,
150
+ statements: undefined,
151
+ lines: undefined,
152
+ branches: undefined,
153
+ functions: undefined,
154
+ },
71
155
  glob: {
72
- test: '**/*.test?(.e2e).{ts,tsx}',
73
- e2e: '**/*.test.e2e.{ts,tsx}',
156
+ test: ['**/*.test{,.e2e,.browser}.{ts,tsx}'],
157
+ browser: ['**/*.test.browser.{ts,tsx}'],
158
+ e2e: ['**/*.test.e2e.{ts,tsx}'],
159
+ exclude: ['node_modules/**'],
74
160
  },
75
- reporter: process.env.CI === 'true' ? 'dot' : 'spec',
76
- type: 'server,e2e',
77
- setup: undefined,
161
+ pool: 'forks',
78
162
  playwrightConfig: undefined,
79
163
  project: undefined,
164
+ reporter: process.env.CI === 'true' ? 'files' : 'spec',
165
+ setup: undefined,
166
+ type: ['server', 'browser', 'e2e'],
80
167
  watch: false,
81
168
  };
82
- export async function loadConfig() {
83
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
84
- console.log(generateHelp());
85
- process.exit(0);
86
- }
87
- let parsed = parseCliArgs();
88
- let fileConfig = await loadConfigFile(parsed.values.config);
169
+ export async function loadConfig(args = process.argv.slice(2), cwd = process.cwd()) {
170
+ let parsed = parseCliArgs(args);
171
+ let fileConfig = await loadConfigFile(parsed.values.config, cwd);
89
172
  let config = resolveConfig(fileConfig, parsed);
90
173
  return config;
91
174
  }
92
- function generateHelp() {
175
+ export function getRemixTestHelpText(_target = process.stdout) {
93
176
  let lines = [
94
- 'Usage: remix-test [glob] [options]',
177
+ 'Usage: remix-test [glob...] [options]',
95
178
  '',
96
179
  'Arguments:',
97
- ` glob Glob pattern for test files (default: "${defaultValues.glob.test}")`,
180
+ ` glob Glob pattern(s) for test files (default: "${defaultValues.glob.test.join(', ')}")`,
98
181
  '',
99
182
  'Options:',
100
183
  ];
@@ -106,47 +189,107 @@ function generateHelp() {
106
189
  lines.push(` ${'-h, --help'.padEnd(30)} Show this help message`);
107
190
  return lines.join('\n');
108
191
  }
109
- function parseCliArgs(args = process.argv.slice(2)) {
192
+ function parseCliArgs(args) {
110
193
  return util.parseArgs({ args, options: cliOptions, allowPositionals: true });
111
194
  }
195
+ function toArray(value) {
196
+ return Array.isArray(value) ? [...value] : [value];
197
+ }
198
+ function toCommaSeparatedArray(value) {
199
+ return toArray(value).flatMap((item) => item
200
+ .split(',')
201
+ .map((part) => part.trim())
202
+ .filter(Boolean));
203
+ }
112
204
  function resolveConfig(fileConfig, { values: cliValues, positionals }) {
205
+ let fileCoverage = typeof fileConfig.coverage === 'boolean' ? {} : fileConfig.coverage || {};
113
206
  return {
114
207
  glob: {
115
- test: positionals[0] ??
116
- cliValues['glob.test'] ??
117
- fileConfig.glob?.test ??
118
- defaultValues.glob.test,
119
- e2e: cliValues['glob.e2e'] ?? fileConfig.glob?.e2e ?? defaultValues.glob.e2e,
208
+ test: toArray(positionals.length > 0
209
+ ? positionals
210
+ : (cliValues['glob.test'] ?? fileConfig.glob?.test ?? defaultValues.glob.test)),
211
+ browser: toArray(cliValues['glob.browser'] ?? fileConfig.glob?.browser ?? defaultValues.glob.browser),
212
+ e2e: toArray(cliValues['glob.e2e'] ?? fileConfig.glob?.e2e ?? defaultValues.glob.e2e),
213
+ exclude: toArray(cliValues['glob.exclude'] ?? fileConfig.glob?.exclude ?? defaultValues.glob.exclude),
120
214
  },
121
215
  browser: {
122
216
  echo: cliValues['browser.echo'] ?? fileConfig.browser?.echo ?? defaultValues.browser.echo,
123
217
  open: cliValues['browser.open'] ?? fileConfig.browser?.open ?? defaultValues.browser.open,
124
218
  },
125
219
  concurrency: Number(cliValues.concurrency ?? fileConfig.concurrency ?? defaultValues.concurrency),
220
+ coverage: cliValues.coverage === true || !!fileConfig.coverage
221
+ ? {
222
+ dir: cliValues['coverage.dir'] ?? fileCoverage.dir ?? defaultValues.coverage.dir,
223
+ include: (() => {
224
+ let raw = cliValues['coverage.include'] ??
225
+ fileCoverage.include ??
226
+ defaultValues.coverage.include;
227
+ return raw === undefined ? undefined : toArray(raw);
228
+ })(),
229
+ exclude: (() => {
230
+ let raw = cliValues['coverage.exclude'] ??
231
+ fileCoverage.exclude ??
232
+ defaultValues.coverage.exclude;
233
+ return raw === undefined ? undefined : toArray(raw);
234
+ })(),
235
+ statements: cliValues['coverage.statements'] !== undefined
236
+ ? Number(cliValues['coverage.statements'])
237
+ : fileCoverage.statements !== undefined
238
+ ? Number(fileCoverage.statements)
239
+ : undefined,
240
+ lines: cliValues['coverage.lines'] !== undefined
241
+ ? Number(cliValues['coverage.lines'])
242
+ : fileCoverage.lines !== undefined
243
+ ? Number(fileCoverage.lines)
244
+ : undefined,
245
+ branches: cliValues['coverage.branches'] !== undefined
246
+ ? Number(cliValues['coverage.branches'])
247
+ : fileCoverage.branches !== undefined
248
+ ? Number(fileCoverage.branches)
249
+ : undefined,
250
+ functions: cliValues['coverage.functions'] !== undefined
251
+ ? Number(cliValues['coverage.functions'])
252
+ : fileCoverage.functions !== undefined
253
+ ? Number(fileCoverage.functions)
254
+ : undefined,
255
+ }
256
+ : undefined,
126
257
  setup: cliValues.setup ?? fileConfig.setup ?? defaultValues.setup,
127
258
  playwrightConfig: cliValues.playwrightConfig ?? fileConfig.playwrightConfig ?? defaultValues.playwrightConfig,
128
- project: cliValues.project ?? fileConfig.project ?? defaultValues.project,
259
+ pool: resolvePool(cliValues.pool ?? fileConfig.pool ?? defaultValues.pool),
260
+ project: (() => {
261
+ let raw = cliValues.project ?? fileConfig.project ?? defaultValues.project;
262
+ return raw === undefined ? undefined : toCommaSeparatedArray(raw);
263
+ })(),
129
264
  reporter: cliValues.reporter ?? fileConfig.reporter ?? defaultValues.reporter,
130
- type: cliValues.type ?? fileConfig.type ?? defaultValues.type,
265
+ type: toCommaSeparatedArray(cliValues.type ?? fileConfig.type ?? defaultValues.type),
131
266
  watch: cliValues.watch ?? fileConfig.watch ?? defaultValues.watch,
132
267
  };
133
268
  }
134
- async function loadConfigFile(configPath) {
269
+ function resolvePool(value) {
270
+ if (value === 'forks' || value === 'threads') {
271
+ return value;
272
+ }
273
+ throw new Error(`Unsupported test pool "${value}". Supported pools are: forks, threads`);
274
+ }
275
+ async function loadConfigFile(configPath, cwd) {
135
276
  let candidates = configPath
136
- ? [path.resolve(process.cwd(), configPath)]
137
- : [
138
- path.join(process.cwd(), 'remix-test.config.ts'),
139
- path.join(process.cwd(), 'remix-test.config.js'),
140
- ];
277
+ ? [path.resolve(cwd, configPath)]
278
+ : [path.join(cwd, 'remix-test.config.ts'), path.join(cwd, 'remix-test.config.js')];
141
279
  for (let candidate of candidates) {
142
280
  try {
143
281
  await fsp.access(candidate);
144
- let mod = await tsImport(candidate, { parentURL: import.meta.url });
145
- return mod.default ?? mod;
146
282
  }
147
283
  catch {
148
- // not found or failed to load — try next
284
+ // not found — try the next candidate
285
+ continue;
149
286
  }
287
+ // The file exists; let import errors propagate rather than silently
288
+ // falling through to defaults — that masking is what hid "Windows
289
+ // absolute paths aren't valid ESM specifiers" by classifying every
290
+ // browser test as a server test.
291
+ let mod = await importModule(candidate, import.meta);
292
+ return mod.default ?? mod;
150
293
  }
151
294
  return {};
152
295
  }
@@ -1,7 +1,17 @@
1
1
  import type { Browser, Page } from 'playwright';
2
- import { type MockFunction, type MockCall, type MockContext } from './mock.ts';
3
- import type { CreateServerFunction } from './e2e-server.ts';
2
+ import type { V8CoverageEntry } from './coverage.ts';
3
+ import { type FakeTimers } from './fake-timers.ts';
4
+ import { type MockCall, type MockContext, type MockFunction } from './mock.ts';
4
5
  import type { getPlaywrightPageOptions } from './playwright.ts';
6
+ /**
7
+ * The shape `t.serve()` consumes. Matches the result of `createTestServer`
8
+ * from `@remix-run/node-fetch-server/test`, but any object with a `baseUrl`
9
+ * and async `close()` works.
10
+ */
11
+ export interface TestServer {
12
+ baseUrl: string;
13
+ close(): Promise<void>;
14
+ }
5
15
  /**
6
16
  * Test Context providing utilities for testing via remix-test. The context is
7
17
  * passed as the first argument to the {@link test}/{@link it} functions.
@@ -49,21 +59,35 @@ export interface TestContext {
49
59
  method<T extends object, K extends keyof T>(obj: T, methodName: K, impl?: Function): MockFunction;
50
60
  };
51
61
  /**
52
- * Starts a test server with the provided request handler.
62
+ * Activates fake timers for testing time-dependent code.
53
63
  *
54
- * @param {(req: Request) => Promise<Response>} handler - Function handling incoming requests
55
- * @returns {Promise<Page>} A promise resolving to a page instance for the server
64
+ * @returns {FakeTimers} A fake timers instance for controlling time
56
65
  */
57
- serve(handler: (req: Request) => Promise<Response>): Promise<Page>;
66
+ useFakeTimers(): FakeTimers;
67
+ /**
68
+ * Wires a running test server up to a Playwright page so the test can drive
69
+ * it. The server is closed automatically when the test ends. Pair with
70
+ * `createTestServer` from `@remix-run/node-fetch-server/test` (or any other
71
+ * source of a `{ baseUrl, close }` handle) to spin up the server first.
72
+ *
73
+ * @param server - The running server the page should target
74
+ * @returns A `Page` whose `baseURL` is set to `server.baseUrl`.
75
+ */
76
+ serve(server: TestServer): Promise<Page>;
77
+ }
78
+ export interface CreateTestContextOptions {
79
+ addE2ECoverageEntries: (value: {
80
+ entries: V8CoverageEntry[];
81
+ baseUrl: string;
82
+ }) => void;
83
+ browser: Browser;
84
+ coverage: boolean;
85
+ open: boolean;
86
+ playwrightPageOptions: ReturnType<typeof getPlaywrightPageOptions>;
58
87
  }
59
- export declare function createTestContext(options: {
60
- createServer?: CreateServerFunction;
61
- browser?: Browser;
62
- open?: boolean;
63
- playwrightPageOptions?: ReturnType<typeof getPlaywrightPageOptions>;
64
- }): {
88
+ export declare function createTestContext(options?: CreateTestContextOptions): {
65
89
  testContext: TestContext;
66
90
  cleanup(): Promise<void>;
67
91
  };
68
- export type { MockFunction, MockCall, MockContext };
92
+ export type { MockCall, MockContext, MockFunction };
69
93
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/lib/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAQ,KAAK,YAAY,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAEpF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAA;AAE/D;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAE3B;;;OAGG;IACH,IAAI,EAAE;QACJ;;;;;;WAMG;QACH,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;QAEhE;;;;;;;;;;WAUG;QACH,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EACxC,GAAG,EAAE,CAAC,EACN,UAAU,EAAE,CAAC,EACb,IAAI,CAAC,EAAE,QAAQ,GACd,YAAY,CAAA;KAChB,CAAA;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IACzC,YAAY,CAAC,EAAE,oBAAoB,CAAA;IACnC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,qBAAqB,CAAC,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAA;CACpE,GAAG;IAAE,WAAW,EAAE,WAAW,CAAC;IAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAkDzD;AAED,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/lib/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACpE,OAAO,EAAQ,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,WAAW,CAAA;AACpF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAA;AAE/D;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAE3B;;;OAGG;IACH,IAAI,EAAE;QACJ;;;;;;WAMG;QACH,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;QAEhE;;;;;;;;;;WAUG;QACH,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EACxC,GAAG,EAAE,CAAC,EACN,UAAU,EAAE,CAAC,EACb,IAAI,CAAC,EAAE,QAAQ,GACd,YAAY,CAAA;KAChB,CAAA;IAED;;;;OAIG;IACH,aAAa,IAAI,UAAU,CAAA;IAE3B;;;;;;;;OAQG;IACH,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACzC;AAED,MAAM,WAAW,wBAAwB;IACvC,qBAAqB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,eAAe,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACvF,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,IAAI,EAAE,OAAO,CAAA;IACb,qBAAqB,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAA;CACnE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG;IACrE,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB,CAkEA;AAED,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { createFakeTimers } from "./fake-timers.js";
1
2
  import { mock } from "./mock.js";
2
3
  export function createTestContext(options) {
3
4
  let cleanups = [];
@@ -14,11 +15,15 @@ export function createTestContext(options) {
14
15
  after(fn) {
15
16
  cleanups.push(fn);
16
17
  },
17
- async serve(handler) {
18
- if (!options.createServer || !options.browser) {
18
+ useFakeTimers() {
19
+ let timers = createFakeTimers();
20
+ cleanups.push(timers.restore);
21
+ return timers;
22
+ },
23
+ async serve(server) {
24
+ if (!options || !options.browser) {
19
25
  throw new Error('t.serve() is only available in E2E test suites');
20
26
  }
21
- let server = await options.createServer(handler);
22
27
  let page = await options.browser.newPage({
23
28
  ...options.playwrightPageOptions,
24
29
  baseURL: server.baseUrl,
@@ -29,6 +34,17 @@ export function createTestContext(options) {
29
34
  if (options.playwrightPageOptions?.actionTimeout != null) {
30
35
  page.setDefaultTimeout(options.playwrightPageOptions.actionTimeout);
31
36
  }
37
+ let coverageEnabled = options.coverage && options.browser.browserType().name() === 'chromium';
38
+ if (coverageEnabled) {
39
+ await page.coverage.startJSCoverage({ resetOnNavigation: false });
40
+ cleanups.push(async () => {
41
+ let entries = await page.coverage.stopJSCoverage();
42
+ options.addE2ECoverageEntries?.({
43
+ entries: entries,
44
+ baseUrl: server.baseUrl,
45
+ });
46
+ });
47
+ }
32
48
  cleanups.push(async () => {
33
49
  if (!options.open) {
34
50
  await page.close();
@@ -0,0 +1,16 @@
1
+ export declare function load(url: string, context: {
2
+ format?: string;
3
+ }, nextLoad: (url: string, context: {
4
+ format?: string;
5
+ }) => Promise<{
6
+ format: string;
7
+ source: string;
8
+ }>): Promise<{
9
+ format: string;
10
+ source: string;
11
+ } | {
12
+ format: string;
13
+ source: string;
14
+ shortCircuit: boolean;
15
+ }>;
16
+ //# sourceMappingURL=coverage-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage-loader.d.ts","sourceRoot":"","sources":["../../src/lib/coverage-loader.ts"],"names":[],"mappings":"AAYA,wBAAsB,IAAI,CACxB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5B,QAAQ,EAAE,CACR,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,KACzB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;;;;;;;GAYjD"}
@@ -0,0 +1,20 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { transformTypeScript } from "./ts-transform.js";
4
+ // Custom ESM loader hook for TypeScript files.
5
+ //
6
+ // Replaces tsx's minified transformation with an un-minified esbuild transform
7
+ // that preserves line structure. This ensures V8 coverage byte offsets map
8
+ // cleanly to TypeScript source lines via the inline source map, giving
9
+ // accurate per-line coverage rather than collapsing multiple statements onto
10
+ // a single minified line.
11
+ export async function load(url, context, nextLoad) {
12
+ let cleanUrl = url.includes('?') ? url.slice(0, url.indexOf('?')) : url;
13
+ if (!cleanUrl.endsWith('.ts') && !cleanUrl.endsWith('.tsx')) {
14
+ return nextLoad(url, context);
15
+ }
16
+ let filePath = fileURLToPath(cleanUrl);
17
+ let source = await readFile(filePath, 'utf-8');
18
+ let { code } = await transformTypeScript(source, filePath);
19
+ return { format: 'module', source: code, shortCircuit: true };
20
+ }
@@ -0,0 +1,28 @@
1
+ import type { createCoverageMap as CreateCoverageMap } from 'istanbul-lib-coverage';
2
+ export interface CoverageConfig {
3
+ dir: string;
4
+ include?: string[];
5
+ exclude?: string[];
6
+ statements?: number;
7
+ lines?: number;
8
+ branches?: number;
9
+ functions?: number;
10
+ }
11
+ export interface V8CoverageEntry {
12
+ url: string;
13
+ source?: string;
14
+ functions: Array<{
15
+ functionName: string;
16
+ isBlockCoverage: boolean;
17
+ ranges: Array<{
18
+ startOffset: number;
19
+ endOffset: number;
20
+ count: number;
21
+ }>;
22
+ }>;
23
+ }
24
+ export type CoverageMap = ReturnType<typeof CreateCoverageMap>;
25
+ export declare function collectServerCoverageMap(coverageDataDir: string, cwd: string, testFiles: Set<string>): Promise<CoverageMap | null>;
26
+ export declare function collectCoverageMapFromPlaywright(entries: V8CoverageEntry[], rootDir: string, testFiles: Set<string>, resolveRelativePath: (url: string) => Promise<string | null>): Promise<CoverageMap | null>;
27
+ export declare function generateCombinedCoverageReport(maps: (CoverageMap | null | undefined)[], cwd: string, config: CoverageConfig): Promise<boolean>;
28
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/lib/coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,IAAI,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAsCnF,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,KAAK,CAAC;QACf,YAAY,EAAE,MAAM,CAAA;QACpB,eAAe,EAAE,OAAO,CAAA;QACxB,MAAM,EAAE,KAAK,CAAC;YAAE,WAAW,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KACzE,CAAC,CAAA;CACH;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAmI9D,wBAAsB,wBAAwB,CAC5C,eAAe,EAAE,MAAM,EACvB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,GACrB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAuD7B;AAED,wBAAsB,gCAAgC,CACpD,OAAO,EAAE,eAAe,EAAE,EAC1B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,EACtB,mBAAmB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAC3D,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA2C7B;AAED,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,CAAC,WAAW,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EACxC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,OAAO,CAAC,CAelB"}