@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
package/dist/cli.js CHANGED
@@ -1,171 +1,350 @@
1
- #!/usr/bin/env node
2
1
  import * as fsp from 'node:fs/promises';
3
2
  import * as path from 'node:path';
4
- import { tsImport } from 'tsx/esm/api';
5
- import { runServerTests } from "./lib/runner.js";
3
+ import { getRemixTestHelpText, IS_RUNNING_FROM_SRC, loadConfig, } from "./lib/config.js";
4
+ import { generateCombinedCoverageReport } from "./lib/coverage.js";
6
5
  import { createReporter } from "./lib/reporters/index.js";
6
+ import { runServerTests } from "./lib/runner.js";
7
7
  import { createWatcher } from "./lib/watcher.js";
8
- import { loadPlaywrightConfig, resolveProjects } from "./lib/playwright.js";
9
- import { loadConfig } from "./lib/config.js";
10
- const config = await loadConfig();
11
- let hasExited = false;
12
- let latestExitCode = 0;
13
- let watcher;
14
- let running = false;
15
- let queued = false;
16
- let rerunTimer;
17
- process.on('SIGINT', () => cleanupAndExit(latestExitCode));
18
- process.on('SIGTERM', () => cleanupAndExit(latestExitCode));
19
- try {
20
- await executeRun();
21
- if (config.watch) {
22
- console.log('Watching for changes. Press Ctrl+C to stop.');
8
+ import { importModule } from "./lib/import-module.js";
9
+ import { IS_BUN } from "./lib/runtime.js";
10
+ import { isMainThread } from 'node:worker_threads';
11
+ export { getRemixTestHelpText };
12
+ const MISSING_PLAYWRIGHT_MESSAGE = 'Playwright is required to run browser and E2E tests. Install it with `npm i -D playwright`.';
13
+ export async function runRemixTest(options = {}) {
14
+ let argv = options.argv ?? process.argv.slice(2);
15
+ let cwd = await resolveCwd(options.cwd ?? process.cwd());
16
+ let previousCwd = process.cwd();
17
+ if (!isMainThread) {
18
+ return await runRemixTestInCwd(argv, cwd);
23
19
  }
24
- }
25
- catch {
26
- cleanupAndExit(1);
27
- }
28
- async function executeRun() {
29
- if (hasExited)
30
- return;
31
- running = true;
32
- let globalTeardown;
33
20
  try {
34
- if (config.setup) {
35
- let mod = await tsImport(path.resolve(process.cwd(), config.setup), {
36
- parentURL: import.meta.url,
37
- });
38
- let globalSetup = mod.globalSetup;
39
- globalTeardown = mod.globalTeardown;
40
- await globalSetup?.();
41
- }
42
- let { files, serverFiles, e2eFiles } = await discoverTests(config);
43
- if (config.watch) {
44
- watcher ??= createWatcher((file) => queueRerun(file));
45
- watcher.update(files);
46
- }
47
- let playwrightConfig = config.playwrightConfig == null || typeof config.playwrightConfig === 'string'
48
- ? await loadPlaywrightConfig(config.playwrightConfig)
49
- : config.playwrightConfig;
50
- let reporter = createReporter(config.reporter);
51
- let startTime = performance.now();
52
- let counts = {
53
- passed: 0,
54
- failed: 0,
55
- skipped: 0,
56
- todo: 0,
57
- };
58
- // Run server tests
59
- if (serverFiles.length > 0) {
60
- reporter.onSectionStart('\nRunning server tests:');
61
- let serverResult = await runServerTests(serverFiles, reporter, config.concurrency, 'server');
62
- counts.failed += serverResult.failed;
63
- counts.passed += serverResult.passed;
64
- counts.skipped += serverResult.skipped;
65
- counts.todo += serverResult.todo;
66
- }
67
- // Run e2e tests for all browsers configured by the user
68
- if (e2eFiles.length > 0) {
69
- let projects = resolveProjects(playwrightConfig);
70
- if (config.project) {
71
- let projectNames = config.project.split(',').map((p) => p.trim());
72
- projects = projects.filter((p) => p.name && projectNames.includes(p.name));
73
- if (projects.length === 0) {
74
- throw new Error(`No playwright projects found with name(s) "${config.project}"`);
75
- }
21
+ process.chdir(cwd);
22
+ return await runRemixTestInCwd(argv, cwd);
23
+ }
24
+ finally {
25
+ process.chdir(previousCwd);
26
+ }
27
+ }
28
+ async function runRemixTestInCwd(argv, cwd) {
29
+ if (argv.includes('--help') || argv.includes('-h')) {
30
+ console.log(getRemixTestHelpText());
31
+ return 0;
32
+ }
33
+ let config = await loadConfig(argv, cwd);
34
+ let hasExited = false;
35
+ let latestExitCode = 0;
36
+ let watcher;
37
+ let running = false;
38
+ let queued = false;
39
+ let rerunTimer;
40
+ let browserServer;
41
+ let browserServerFilesKey;
42
+ let browserPort;
43
+ let resolveRun;
44
+ let runPromise = new Promise((resolve) => {
45
+ resolveRun = resolve;
46
+ });
47
+ let cleanupAndExit = (code) => {
48
+ if (hasExited)
49
+ return;
50
+ hasExited = true;
51
+ watcher?.close();
52
+ browserServer?.close();
53
+ clearTimeout(rerunTimer);
54
+ process.off('SIGINT', handleInterrupt);
55
+ process.off('SIGTERM', handleInterrupt);
56
+ resolveRun?.(code);
57
+ };
58
+ let handleInterrupt = () => cleanupAndExit(latestExitCode);
59
+ let closeBrowserServer = async () => {
60
+ if (!browserServer)
61
+ return;
62
+ let server = browserServer;
63
+ await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
64
+ browserServer = undefined;
65
+ browserServerFilesKey = undefined;
66
+ browserPort = undefined;
67
+ };
68
+ let queueRerun = (reason) => {
69
+ if (!config.watch || hasExited)
70
+ return;
71
+ clearTimeout(rerunTimer);
72
+ rerunTimer = setTimeout(() => {
73
+ rerunTimer = undefined;
74
+ if (running) {
75
+ queued = true;
76
76
  }
77
- for (let project of projects) {
78
- reporter.onSectionStart(`\nRunning tests for project \`${project.name}\`:`);
79
- if (config.browser?.open) {
80
- if (project.playwrightUseOpts?.headless === true) {
81
- let label = project.name ? ` (project "${project.name}")` : '';
82
- console.warn(`Warning: browser.open is set but playwright headless is explicitly true${label} — ignoring browser.open`);
77
+ else {
78
+ console.log(`\n↻ Change detected (${reason}), re-running tests...\n`);
79
+ void executeRun();
80
+ }
81
+ }, 100);
82
+ };
83
+ let executeRun = async () => {
84
+ if (hasExited)
85
+ return;
86
+ running = true;
87
+ let globalTeardown;
88
+ try {
89
+ if (config.setup) {
90
+ let mod = await importModule(path.resolve(cwd, config.setup), import.meta);
91
+ let globalSetup = mod.globalSetup;
92
+ globalTeardown = mod.globalTeardown;
93
+ await globalSetup?.();
94
+ }
95
+ let discoveredTests = await discoverTests(config, cwd);
96
+ if (discoveredTests == null) {
97
+ latestExitCode = 1;
98
+ cleanupAndExit(latestExitCode);
99
+ return;
100
+ }
101
+ let { files, serverFiles, browserFiles, e2eFiles } = discoveredTests;
102
+ if (config.watch) {
103
+ watcher ??= createWatcher((file) => queueRerun(file));
104
+ watcher.update(files);
105
+ }
106
+ let browserFilesKey = browserFiles.join('\0');
107
+ if (browserServer && browserFiles.length === 0) {
108
+ await closeBrowserServer();
109
+ }
110
+ else if (browserFiles.length > 0 &&
111
+ (!browserServer || browserServerFilesKey !== browserFilesKey)) {
112
+ await closeBrowserServer();
113
+ let { startServer } = IS_RUNNING_FROM_SRC
114
+ ? await importModule('./app/server.ts', import.meta)
115
+ : await import(`./app/server.js`);
116
+ let result = await startServer(browserFiles);
117
+ browserServer = result.server;
118
+ browserServerFilesKey = browserFilesKey;
119
+ browserPort = result.port;
120
+ }
121
+ let reporter = createReporter(config.reporter);
122
+ let startTime = performance.now();
123
+ let counts = {
124
+ passed: 0,
125
+ failed: 0,
126
+ skipped: 0,
127
+ todo: 0,
128
+ };
129
+ let allCoverageMaps = [];
130
+ if (serverFiles.length > 0) {
131
+ reporter.onSectionStart('\nRunning server tests:');
132
+ let serverResult = await runServerTests(serverFiles, reporter, config.concurrency, 'server', {
133
+ coverage: config.coverage,
134
+ cwd,
135
+ pool: config.pool,
136
+ });
137
+ counts.failed += serverResult.failed;
138
+ counts.passed += serverResult.passed;
139
+ counts.skipped += serverResult.skipped;
140
+ counts.todo += serverResult.todo;
141
+ allCoverageMaps.push(serverResult.coverageMap);
142
+ }
143
+ // Run browser/e2e tests for all browsers configured by the user
144
+ if (browserFiles.length > 0 || e2eFiles.length > 0) {
145
+ let { loadPlaywrightConfig, resolveProjects } = await importPlaywrightSupport();
146
+ let runBrowserTests = browserFiles.length > 0 ? (await importBrowserTestRunner()).runBrowserTests : undefined;
147
+ let playwrightConfig = config.playwrightConfig == null || typeof config.playwrightConfig === 'string'
148
+ ? await loadPlaywrightConfig(config.playwrightConfig, cwd)
149
+ : config.playwrightConfig;
150
+ let projects = resolveProjects(playwrightConfig);
151
+ if (config.project) {
152
+ let projectNames = new Set(config.project);
153
+ projects = projects.filter((project) => project.name && projectNames.has(project.name));
154
+ if (projects.length === 0) {
155
+ throw new Error(`No playwright projects found with name(s) "${config.project.join(', ')}"`);
156
+ }
157
+ }
158
+ let lastBrowserResult = null;
159
+ for (let project of projects) {
160
+ reporter.onSectionStart(`\nRunning tests for project \`${project.name}\`:`);
161
+ if (config.browser?.open) {
162
+ if (project.playwrightUseOpts?.headless === true) {
163
+ let label = project.name ? ` (project "${project.name}")` : '';
164
+ console.warn(`Warning: browser.open is set but playwright headless is explicitly true${label} — ignoring browser.open`);
165
+ }
166
+ else {
167
+ project.playwrightUseOpts = { ...project.playwrightUseOpts, headless: false };
168
+ }
83
169
  }
84
- else {
85
- project.playwrightUseOpts = { ...project.playwrightUseOpts, headless: false };
170
+ let [browserResult, e2eResult] = await Promise.all([
171
+ runBrowserTests != null
172
+ ? runBrowserTests({
173
+ baseUrl: `http://localhost:${browserPort}`,
174
+ console: config.browser?.echo,
175
+ coverage: !!config.coverage,
176
+ open: config.browser?.open,
177
+ playwrightUseOpts: project.playwrightUseOpts,
178
+ projectName: project.name,
179
+ reporter,
180
+ testFiles: browserFiles,
181
+ })
182
+ : null,
183
+ e2eFiles.length > 0
184
+ ? runServerTests(e2eFiles, reporter, config.concurrency, 'e2e', {
185
+ open: config.browser?.open,
186
+ playwrightUseOpts: project.playwrightUseOpts,
187
+ projectName: project.name,
188
+ coverage: config.coverage,
189
+ cwd,
190
+ pool: config.pool,
191
+ })
192
+ : null,
193
+ ]);
194
+ counts.passed += (browserResult?.results.passed ?? 0) + (e2eResult?.passed ?? 0);
195
+ counts.failed += (browserResult?.results.failed ?? 0) + (e2eResult?.failed ?? 0);
196
+ counts.skipped += (browserResult?.results.skipped ?? 0) + (e2eResult?.skipped ?? 0);
197
+ counts.todo += (browserResult?.results.todo ?? 0) + (e2eResult?.todo ?? 0);
198
+ allCoverageMaps.push(browserResult?.coverageMap);
199
+ allCoverageMaps.push(e2eResult?.coverageMap);
200
+ if (browserResult) {
201
+ lastBrowserResult = browserResult;
86
202
  }
87
203
  }
88
- let e2eResult = e2eFiles.length > 0
89
- ? await runServerTests(e2eFiles, reporter, config.concurrency, 'e2e', {
90
- open: config.browser?.open,
91
- playwrightUseOpts: project.playwrightUseOpts,
92
- projectName: project.name,
93
- })
94
- : null;
95
- counts.passed += e2eResult?.passed ?? 0;
96
- counts.failed += e2eResult?.failed ?? 0;
97
- counts.skipped += e2eResult?.skipped ?? 0;
98
- counts.todo += e2eResult?.todo ?? 0;
204
+ if (config.browser?.open && lastBrowserResult) {
205
+ console.log('\nBrowser is open. Press Ctrl+C to close.');
206
+ await Promise.race([
207
+ lastBrowserResult.disconnected,
208
+ new Promise((resolve) => {
209
+ process.once('SIGINT', resolve);
210
+ process.once('SIGTERM', resolve);
211
+ }),
212
+ ]);
213
+ await lastBrowserResult.close();
214
+ }
215
+ }
216
+ reporter.onSummary(counts, performance.now() - startTime);
217
+ let thresholdsPassed = true;
218
+ if (config.coverage) {
219
+ thresholdsPassed = await generateCombinedCoverageReport(allCoverageMaps, cwd, config.coverage);
220
+ }
221
+ latestExitCode = counts.failed > 0 || !thresholdsPassed ? 1 : 0;
222
+ }
223
+ catch (error) {
224
+ console.error('Error running tests:', error);
225
+ latestExitCode = 1;
226
+ }
227
+ finally {
228
+ await globalTeardown?.();
229
+ running = false;
230
+ if (queued) {
231
+ queued = false;
232
+ queueRerun('queued change');
233
+ }
234
+ else if (!config.watch) {
235
+ cleanupAndExit(latestExitCode);
99
236
  }
100
237
  }
101
- reporter.onSummary(counts, performance.now() - startTime);
102
- latestExitCode = counts.failed > 0 ? 1 : 0;
238
+ };
239
+ process.on('SIGINT', handleInterrupt);
240
+ process.on('SIGTERM', handleInterrupt);
241
+ try {
242
+ await executeRun();
243
+ if (config.watch && !hasExited) {
244
+ console.log('Watching for changes. Press Ctrl+C to stop.');
245
+ }
246
+ }
247
+ catch {
248
+ cleanupAndExit(1);
249
+ }
250
+ return await runPromise;
251
+ }
252
+ async function importPlaywrightSupport() {
253
+ try {
254
+ return await import("./lib/playwright.js");
103
255
  }
104
256
  catch (error) {
105
- console.error('Error running tests:', error);
106
- latestExitCode = 1;
257
+ throw toPlaywrightImportError(error);
107
258
  }
108
- finally {
109
- await globalTeardown?.();
110
- running = false;
111
- if (queued) {
112
- queued = false;
113
- queueRerun('queued change');
114
- }
115
- else if (!config.watch) {
116
- cleanupAndExit(latestExitCode);
117
- }
259
+ }
260
+ async function importBrowserTestRunner() {
261
+ try {
262
+ return await import("./lib/runner-browser.js");
263
+ }
264
+ catch (error) {
265
+ throw toPlaywrightImportError(error);
118
266
  }
119
267
  }
120
- async function discoverTests(config) {
121
- async function findFiles(pattern) {
122
- let files = [];
123
- let exclude = ['node_modules/**', '.git/**'];
124
- for await (let file of fsp.glob(pattern, { cwd: process.cwd(), exclude })) {
125
- files.push(path.resolve(process.cwd(), file));
126
- }
127
- return files;
268
+ function toPlaywrightImportError(error) {
269
+ return isMissingPlaywrightImport(error) ? new Error(MISSING_PLAYWRIGHT_MESSAGE) : error;
270
+ }
271
+ function isMissingPlaywrightImport(error) {
272
+ if (!isRecord(error) || typeof error.message !== 'string') {
273
+ return false;
274
+ }
275
+ return ((error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND') &&
276
+ (error.message.includes("Cannot find package 'playwright'") ||
277
+ error.message.includes("Cannot find module 'playwright'")));
278
+ }
279
+ function isRecord(value) {
280
+ return typeof value === 'object' && value !== null;
281
+ }
282
+ async function resolveCwd(cwd) {
283
+ try {
284
+ return await fsp.realpath(cwd);
128
285
  }
129
- let files = await findFiles(config.glob.test);
286
+ catch {
287
+ return path.resolve(cwd);
288
+ }
289
+ }
290
+ async function discoverTests(config, cwd) {
291
+ let files = await findFiles(config.glob.test, config.glob.exclude, cwd);
130
292
  if (files.length === 0) {
131
- console.log(`No test files found matching pattern: ${config.glob.test}`);
132
- process.exit(1);
133
- }
134
- let e2eSet = new Set(await findFiles(config.glob.e2e));
135
- let types = new Set(config.type.split(','));
136
- let e2eFiles = types.has('e2e') ? files.filter((f) => e2eSet.has(f)) : [];
137
- let serverFiles = types.has('server') ? files.filter((f) => !e2eSet.has(f)) : [];
138
- let totalFiles = serverFiles.length + e2eFiles.length;
293
+ console.log(`No test files found matching pattern: ${config.glob.test.join(', ')}`);
294
+ return null;
295
+ }
296
+ let browserSet = new Set(await findFiles(config.glob.browser, config.glob.exclude, cwd));
297
+ let e2eSet = new Set(await findFiles(config.glob.e2e, config.glob.exclude, cwd));
298
+ let types = new Set(config.type);
299
+ let browserFiles = types.has('browser') ? files.filter((f) => browserSet.has(f)) : [];
300
+ let e2eFiles = types.has('e2e') ? files.filter((file) => e2eSet.has(file)) : [];
301
+ let serverFiles = types.has('server')
302
+ ? files.filter((file) => !browserSet.has(file) && !e2eSet.has(file))
303
+ : [];
304
+ let totalFiles = browserFiles.length + serverFiles.length + e2eFiles.length;
139
305
  if (totalFiles === 0) {
140
- console.log(`No test files remain after filtering for type ${config.type}`);
141
- process.exit(1);
306
+ console.log(`No test files remain after filtering for type ${config.type.join(', ')}`);
307
+ return null;
142
308
  }
143
- console.log(`Found ${totalFiles} test file(s) (${serverFiles.length} server, ${e2eFiles.length} e2e)`);
309
+ console.log(`Found ${totalFiles} test file(s) (${serverFiles.length} server, ${browserFiles.length} browser, ${e2eFiles.length} e2e)`);
144
310
  return {
145
311
  files,
146
312
  serverFiles,
313
+ browserFiles,
147
314
  e2eFiles,
148
315
  };
149
316
  }
150
- function queueRerun(reason) {
151
- if (!config.watch || hasExited)
152
- return;
153
- clearTimeout(rerunTimer);
154
- rerunTimer = setTimeout(() => {
155
- rerunTimer = undefined;
156
- if (running) {
157
- queued = true;
317
+ async function findFiles(patterns, excludePatterns, cwd) {
318
+ let files = new Set();
319
+ if (IS_BUN) {
320
+ // Bun's `fs.promises.glob` follows symlinks and doesn't prune traversal
321
+ // via `exclude`, so it enters pnpm symlink cycles in `node_modules`.
322
+ // Use Bun's native Glob, which defaults to `followSymlinks: false`.
323
+ // @ts-expect-error — bun module is only resolvable under the Bun runtime
324
+ let { Glob } = await import('bun');
325
+ let excludeGlobs = excludePatterns.map((p) => new Glob(p));
326
+ for (let pattern of patterns) {
327
+ let glob = new Glob(pattern);
328
+ for await (let file of glob.scan({ cwd, absolute: true })) {
329
+ let rel = toPosix(path.relative(cwd, file));
330
+ if (!excludeGlobs.some((eg) => eg.match(rel))) {
331
+ files.add(toPosix(file));
332
+ }
333
+ }
158
334
  }
159
- else {
160
- console.log(`\n↻ Change detected (${reason}), re-running tests...\n`);
161
- void executeRun();
335
+ return [...files];
336
+ }
337
+ for (let pattern of patterns) {
338
+ for await (let file of fsp.glob(pattern, { cwd, exclude: excludePatterns })) {
339
+ files.add(toPosix(path.resolve(cwd, file)));
162
340
  }
163
- }, 100);
341
+ }
342
+ return [...files];
164
343
  }
165
- function cleanupAndExit(code) {
166
- if (hasExited)
167
- return;
168
- hasExited = true;
169
- watcher?.close();
170
- process.exit(code);
344
+ // Normalize discovered paths so set membership across the test/browser/e2e
345
+ // `findFiles` calls is byte-stable on every platform. Node accepts forward
346
+ // slashes for filesystem operations on Windows, so downstream `fs.readFile`
347
+ // etc. work without further conversion.
348
+ function toPosix(p) {
349
+ return path.sep === '/' ? p : p.replace(/\\/g, '/');
171
350
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- export type { RemixTestConfig } from './lib/config.ts';
1
+ export type { RemixTestConfig, RemixTestPool } from './lib/config.ts';
2
2
  export { describe, it, suite, test, before, after, beforeEach, afterEach, beforeAll, afterAll, } from './lib/framework.ts';
3
3
  export { mock } from './lib/mock.ts';
4
4
  export type { TestContext } from './lib/context.ts';
5
+ export type { FakeTimers } from './lib/fake-timers.ts';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,EACL,QAAQ,EACR,EAAE,EACF,KAAK,EACL,IAAI,EACJ,MAAM,EACN,KAAK,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,QAAQ,GACT,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACpC,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,EACL,QAAQ,EACR,EAAE,EACF,KAAK,EACL,IAAI,EACJ,MAAM,EACN,KAAK,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,QAAQ,GACT,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACpC,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export declare const colors: import("@remix-run/terminal").TerminalStyles;
2
+ //# sourceMappingURL=colors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../../src/lib/colors.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM,8CAAiB,CAAA"}
@@ -0,0 +1,2 @@
1
+ import { createStyles } from '@remix-run/terminal';
2
+ export const colors = createStyles();
@@ -1,4 +1,7 @@
1
1
  import type { PlaywrightTestConfig } from 'playwright/test';
2
+ export declare const IS_RUNNING_FROM_SRC: boolean;
3
+ export declare function getBrowserTestRootDir(): string;
4
+ export type RemixTestPool = 'forks' | 'threads';
2
5
  export interface RemixTestConfig {
3
6
  /**
4
7
  * Options for controlling the playwright browser
@@ -10,16 +13,34 @@ export interface RemixTestConfig {
10
13
  open?: boolean;
11
14
  };
12
15
  /**
13
- * Glob patterns to identify test files
14
- * - `glob.test`: Glob pattern for all test files (--glob.test)
15
- * - `glob.e2e`: Glob pattern for the subset of e2e test files (--glob.e2e)
16
+ * Glob patterns to identify test files. Each field accepts a single pattern
17
+ * or an array of patterns; arrays are unioned during discovery.
18
+ * - `glob.test`: Glob pattern(s) for all test files (--glob.test)
19
+ * - `glob.browser`: Glob pattern(s) for the subset of browser test files (--glob.browser)
20
+ * - `glob.e2e`: Glob pattern(s) for the subset of e2e test files (--glob.e2e)
21
+ * - `glob.exclude`: Glob pattern(s) for paths to exclude from discovery (--glob.exclude)
16
22
  */
17
23
  glob?: {
18
- test?: string;
19
- e2e?: string;
24
+ test?: string | string[];
25
+ browser?: string | string[];
26
+ e2e?: string | string[];
27
+ exclude?: string | string[];
20
28
  };
21
29
  /** Max number of concurrent test workers (--concurrency) */
22
30
  concurrency?: number | string;
31
+ /**
32
+ * Coverage configuration. `true` enables with defaults; an object enables with settings;
33
+ * `false` disables. CLI `--coverage` flag overrides the boolean aspect.
34
+ */
35
+ coverage?: boolean | {
36
+ dir?: string;
37
+ include?: string | string[];
38
+ exclude?: string | string[];
39
+ statements?: number | string;
40
+ lines?: number | string;
41
+ branches?: number | string;
42
+ functions?: number | string;
43
+ };
23
44
  /**
24
45
  * Path to a module that exports `globalSetup` and/or `globalTeardown` functions,
25
46
  * called once before and after the test run respectively. (--setup)
@@ -30,12 +51,23 @@ export interface RemixTestConfig {
30
51
  * PlaywrightTestConfig object. CLI `--playwrightConfig` only accepts a file path.
31
52
  */
32
53
  playwrightConfig?: string | PlaywrightTestConfig;
33
- /** Filter tests to a specific playwright project or comma-separated list of projects (--project) */
34
- project?: string;
54
+ /**
55
+ * Pool used to run server and E2E test files. Forked child processes are the default,
56
+ * but worker threads are available for projects that prefer the previous behavior.
57
+ */
58
+ pool?: RemixTestPool;
59
+ /**
60
+ * Filter tests to specific playwright project(s) (--project). Accepts a single
61
+ * project name or an array of names; `--project` may be repeated on the CLI.
62
+ */
63
+ project?: string | string[];
35
64
  /** Test reporter (--reporter) */
36
65
  reporter?: string;
37
- /** Comma-separated list of test types to run (--type) */
38
- type?: string;
66
+ /**
67
+ * Test type(s) to run (--type). Accepts a single type or an array of types;
68
+ * `--type` may be repeated on the CLI. Valid values: "server", "browser", "e2e".
69
+ */
70
+ type?: string | string[];
39
71
  /** Watch mode — re-run tests on file changes (--watch) */
40
72
  watch?: boolean;
41
73
  }
@@ -45,16 +77,29 @@ export interface ResolvedRemixTestConfig {
45
77
  open?: boolean;
46
78
  };
47
79
  concurrency: number;
80
+ coverage: {
81
+ dir: string;
82
+ include?: string[];
83
+ exclude?: string[];
84
+ statements?: number;
85
+ lines?: number;
86
+ branches?: number;
87
+ functions?: number;
88
+ } | undefined;
48
89
  glob: {
49
- test: string;
50
- e2e: string;
90
+ test: string[];
91
+ browser: string[];
92
+ e2e: string[];
93
+ exclude: string[];
51
94
  };
52
95
  playwrightConfig: string | PlaywrightTestConfig | undefined;
53
- project: string | undefined;
96
+ project: string[] | undefined;
54
97
  reporter: string;
98
+ pool: RemixTestPool;
55
99
  setup: string | undefined;
56
- type: string;
100
+ type: string[];
57
101
  watch: boolean;
58
102
  }
59
- export declare function loadConfig(): Promise<ResolvedRemixTestConfig>;
103
+ export declare function loadConfig(args?: string[], cwd?: string): Promise<ResolvedRemixTestConfig>;
104
+ export declare function getRemixTestHelpText(_target?: NodeJS.WriteStream): string;
60
105
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAgF3D,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;KACf,CAAA;IACD;;;;OAIG;IACH,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,GAAG,CAAC,EAAE,MAAM,CAAA;KACb,CAAA;IACD,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC7B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,oBAAoB,CAAA;IAChD,oGAAoG;IACpG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4DAA0D;IAC1D,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE;QACP,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;KACf,CAAA;IACD,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAA;QACZ,GAAG,EAAE,MAAM,CAAA;KACZ,CAAA;IACD,gBAAgB,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAAA;IAC3D,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,UAAU,qCAU/B"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}