@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.
Files changed (79) hide show
  1. package/README.md +43 -44
  2. package/dist/app/client/entry.js +4 -0
  3. package/dist/app/server.d.ts.map +1 -1
  4. package/dist/app/server.js +10 -10
  5. package/dist/cli.d.ts +30 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +87 -23
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/lib/config.d.ts +55 -21
  11. package/dist/lib/config.d.ts.map +1 -1
  12. package/dist/lib/config.js +82 -33
  13. package/dist/lib/context.d.ts +5 -5
  14. package/dist/lib/coverage-loader.js +2 -2
  15. package/dist/lib/coverage.js +1 -1
  16. package/dist/lib/fake-timers.d.ts +39 -0
  17. package/dist/lib/fake-timers.d.ts.map +1 -1
  18. package/dist/lib/fake-timers.js +27 -8
  19. package/dist/lib/framework.d.ts +12 -6
  20. package/dist/lib/framework.d.ts.map +1 -1
  21. package/dist/lib/framework.js +24 -12
  22. package/dist/lib/import-module.d.ts.map +1 -1
  23. package/dist/lib/import-module.js +13 -3
  24. package/dist/lib/reporters/dot.d.ts.map +1 -1
  25. package/dist/lib/reporters/dot.js +10 -0
  26. package/dist/lib/reporters/files.d.ts.map +1 -1
  27. package/dist/lib/reporters/files.js +10 -0
  28. package/dist/lib/reporters/results.d.ts +1 -1
  29. package/dist/lib/reporters/results.d.ts.map +1 -1
  30. package/dist/lib/reporters/spec.d.ts.map +1 -1
  31. package/dist/lib/reporters/spec.js +10 -0
  32. package/dist/lib/reporters/tap.d.ts.map +1 -1
  33. package/dist/lib/reporters/tap.js +10 -0
  34. package/dist/lib/runner-browser.d.ts.map +1 -1
  35. package/dist/lib/runner-browser.js +40 -2
  36. package/dist/lib/runner.d.ts +18 -1
  37. package/dist/lib/runner.d.ts.map +1 -1
  38. package/dist/lib/runner.js +187 -38
  39. package/dist/lib/worker-e2e-file.d.ts +11 -0
  40. package/dist/lib/worker-e2e-file.d.ts.map +1 -0
  41. package/dist/lib/worker-e2e-file.js +69 -0
  42. package/dist/lib/worker-e2e.js +11 -47
  43. package/dist/lib/worker-process.d.ts +2 -0
  44. package/dist/lib/worker-process.d.ts.map +1 -0
  45. package/dist/lib/worker-process.js +55 -0
  46. package/dist/lib/worker-results.d.ts +3 -0
  47. package/dist/lib/worker-results.d.ts.map +1 -0
  48. package/dist/lib/worker-results.js +20 -0
  49. package/dist/lib/worker-server.d.ts +10 -0
  50. package/dist/lib/worker-server.d.ts.map +1 -0
  51. package/dist/lib/worker-server.js +112 -0
  52. package/dist/lib/worker.js +6 -55
  53. package/package.json +5 -5
  54. package/src/app/client/entry.ts +4 -0
  55. package/src/app/server.ts +11 -10
  56. package/src/cli.ts +121 -28
  57. package/src/index.ts +1 -1
  58. package/src/lib/config.ts +144 -58
  59. package/src/lib/context.ts +5 -5
  60. package/src/lib/coverage-loader.ts +2 -2
  61. package/src/lib/coverage.ts +1 -1
  62. package/src/lib/fake-timers.ts +65 -8
  63. package/src/lib/framework.ts +53 -36
  64. package/src/lib/import-module.ts +14 -3
  65. package/src/lib/reporters/dot.ts +9 -0
  66. package/src/lib/reporters/files.ts +9 -0
  67. package/src/lib/reporters/results.ts +1 -1
  68. package/src/lib/reporters/spec.ts +9 -0
  69. package/src/lib/reporters/tap.ts +9 -0
  70. package/src/lib/runner-browser.ts +46 -2
  71. package/src/lib/runner.ts +253 -50
  72. package/src/lib/ts-transform.ts +1 -1
  73. package/src/lib/worker-e2e-file.ts +98 -0
  74. package/src/lib/worker-e2e.ts +14 -51
  75. package/src/lib/worker-process.ts +69 -0
  76. package/src/lib/worker-results.ts +22 -0
  77. package/src/lib/worker-server.ts +123 -0
  78. package/src/lib/worker.ts +7 -47
  79. package/tsconfig.json +6 -3
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # `test`
1
+ # test
2
2
 
3
3
  A test framework for JavaScript and TypeScript projects.
4
4
 
@@ -41,20 +41,14 @@ Run tests with the CLI:
41
41
  remix test
42
42
  ```
43
43
 
44
- By default, `remix test` discovers all files matching `**/*.test{,.e2e}.{ts,tsx}`. Pass a glob as the first positional argument to override:
44
+ By default, `remix test` discovers all files matching `**/*.test{,.e2e}.{ts,tsx}`. Pass one or more globs as positional arguments to override:
45
45
 
46
46
  ```sh
47
47
  remix test "src/**/*.test.ts"
48
+ remix test "src/**/*.test.ts" "tests/**/*.test.tsx"
48
49
  ```
49
50
 
50
- Or, you may control via the `glob.test` config field/CLI arg.
51
-
52
- If you install `@remix-run/test` directly instead of the umbrella `remix` package, the same runner is available as `remix-test`:
53
-
54
- ```sh
55
- npm i @remix-run/test
56
- remix-test
57
- ```
51
+ Or, you may control via the `glob.test` config field/CLI arg. Each `glob.*` field accepts a single string or an array of patterns, and `--glob.*` flags can be repeated on the CLI.
58
52
 
59
53
  ### Config File
60
54
 
@@ -76,15 +70,18 @@ export default {
76
70
  // Max number of concurrent test workers (default `os.availableParallelism()`)
77
71
  concurrency: 2,
78
72
 
73
+ // Pool for server and E2E test files ("forks", "threads")
74
+ pool: 'forks',
75
+
79
76
  // Code coverage options
80
77
  coverage: {
81
78
  // Enable coverage reporting
82
79
  enabled: true,
83
80
  // Output directory (default: ".coverage")
84
81
  dir: '.coverage',
85
- // Glob patterns to include/exclude
86
- include: ['src/**'],
87
- exclude: ['src/**/*.test.ts'],
82
+ // Glob pattern(s) to include/exclude
83
+ include: 'src/**',
84
+ exclude: 'src/**/*.test.ts',
88
85
  // Minimum thresholds (%)
89
86
  statements: 80,
90
87
  lines: 80,
@@ -92,12 +89,13 @@ export default {
92
89
  functions: 80,
93
90
  },
94
91
 
92
+ // Glob pattern(s) identifying test files
95
93
  glob: {
96
- // Glob pattern identifying all test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}")
94
+ // All test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}").
97
95
  test: '**/*.test{,.browser,.e2e}.ts',
98
- // Glob pattern identifying browser test files (default: "**/*.test.browser.{ts,tsx}")
96
+ // Browser test files (default: "**/*.test.browser.{ts,tsx}")
99
97
  browser: '**/*.test.browser.ts',
100
- // Glob pattern identifying E2E test files (default: "**/*.test.e2e.{ts,tsx}")
98
+ // E2E test files (default: "**/*.test.e2e.{ts,tsx}")
101
99
  e2e: '**/*.test.e2e.ts',
102
100
  },
103
101
 
@@ -114,7 +112,7 @@ export default {
114
112
  },
115
113
  },
116
114
 
117
- // Comma-separated list of playwright projects to run E2E tests for
115
+ // Playwright project(s) to run E2E tests for
118
116
  project: 'chromium',
119
117
 
120
118
  // Test reporter ("spec", "files", "tap", "dot")
@@ -123,8 +121,8 @@ export default {
123
121
  // Path to a setup module (see Setup section below)
124
122
  setup: './test/setup.ts',
125
123
 
126
- // Comma-separated list of test types to run ("server", "browser", "e2e")
127
- type: 'server,browser,e2e',
124
+ // Test type(s) to run ("server", "browser", "e2e")
125
+ type: ['server', 'browser', 'e2e'],
128
126
 
129
127
  // Watch for file changes and re-run
130
128
  watch: false,
@@ -141,28 +139,29 @@ remix test --config ./tests/config.ts
141
139
 
142
140
  You may also specify any config field as a CLI flag which will take precedence over config file values:
143
141
 
144
- | Flag | Short |
145
- | --------------------------- | ----- |
146
- | `--browser.echo` | |
147
- | `--browser.open` | |
148
- | `--concurrency <n>` | `-c` |
149
- | `--coverage` | |
150
- | `--coverage.dir <path>` | |
151
- | `--coverage.include` | |
152
- | `--coverage.exclude` | |
153
- | `--coverage.statements` | |
154
- | `--coverage.lines` | |
155
- | `--coverage.branches` | |
156
- | `--coverage.functions` | |
157
- | `--glob.test` | |
158
- | `--glob.browser` | |
159
- | `--glob.e2e` | |
160
- | `--playwrightConfig <path>` | |
161
- | `--project <name>` | `-p` |
162
- | `--reporter <name>` | `-r` |
163
- | `--setup <path>` | `-s` |
164
- | `--type <name>` | `-t` |
165
- | `--watch` | `-w` |
142
+ | Flag | Short |
143
+ | --------------------------- | --------- | --- |
144
+ | `--browser.echo` | |
145
+ | `--browser.open` | |
146
+ | `--concurrency <n>` | `-c` |
147
+ | `--coverage` | |
148
+ | `--coverage.dir <path>` | |
149
+ | `--coverage.include` | |
150
+ | `--coverage.exclude` | |
151
+ | `--coverage.statements` | |
152
+ | `--coverage.lines` | |
153
+ | `--coverage.branches` | |
154
+ | `--coverage.functions` | |
155
+ | `--glob.test` | |
156
+ | `--glob.browser` | |
157
+ | `--glob.e2e` | |
158
+ | `--playwrightConfig <path>` | |
159
+ | `--pool <forks | threads>` | |
160
+ | `--project <name>` | `-p` |
161
+ | `--reporter <name>` | `-r` |
162
+ | `--setup <path>` | `-s` |
163
+ | `--type <name>` | `-t` |
164
+ | `--watch` | `-w` |
166
165
 
167
166
  ### Setup
168
167
 
@@ -210,11 +209,11 @@ suite('My Test Suite', () => {
210
209
 
211
210
  ### Programmatic runner
212
211
 
213
- `@remix-run/test/cli` exports `runRemixTest()` for tools that want to run the test runner without
212
+ `remix/test/cli` exports `runRemixTest()` for tools that want to run the test runner without
214
213
  exiting the current process:
215
214
 
216
215
  ```ts
217
- import { runRemixTest } from '@remix-run/test/cli'
216
+ import { runRemixTest } from 'remix/test/cli'
218
217
 
219
218
  let exitCode = await runRemixTest({
220
219
  argv: ['--type', 'server'],
@@ -222,7 +221,7 @@ let exitCode = await runRemixTest({
222
221
  })
223
222
  ```
224
223
 
225
- `runRemixTest()` returns the runner exit code. The `remix test` and `remix-test` bin wrappers call
224
+ `runRemixTest()` returns the runner exit code. The `remix test` bin wrapper calls
226
225
  `process.exit()` with that code when the run finishes so open workers, browsers, or project handles
227
226
  cannot keep the CLI alive.
228
227
 
@@ -167,6 +167,10 @@ function runInIframe(testFile) {
167
167
  return new Promise((resolve) => {
168
168
  let iframe = document.createElement('iframe');
169
169
  iframe.src = `/iframe?file=${encodeURIComponent(testFile)}`;
170
+ // Make the iframe as big so we don't get unintentional scrolling in test UIs
171
+ let parentBody = iframe.contentWindow?.document.body;
172
+ iframe.width = Math.max(parentBody?.scrollWidth ?? 0, 800).toString();
173
+ iframe.height = Math.max(Math.round((parentBody?.scrollHeight ?? 0) / 2), 400).toString();
170
174
  document.body.appendChild(iframe);
171
175
  function onMessage(event) {
172
176
  if (event.source !== iframe.contentWindow)
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAQjC,wBAAsB,WAAW,CAC/B,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC;IAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkChD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAWjC,wBAAsB,WAAW,CAC/B,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC;IAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkChD"}
@@ -8,6 +8,8 @@ import { fileURLToPath } from 'node:url';
8
8
  import { SourceMapConsumer, SourceMapGenerator } from 'source-map-js';
9
9
  import { getBrowserTestRootDir, IS_RUNNING_FROM_SRC } from "../lib/config.js";
10
10
  import { transformTypeScript } from "../lib/ts-transform.js";
11
+ const log = (str) => console.log(`[remix:test] ${str}`);
12
+ const logError = (str, e) => console.error(`[remix:test] Error: ${str}\n`, e);
11
13
  export async function startServer(browserFiles) {
12
14
  let handle = createRequestHandler(browserFiles);
13
15
  let port = 44101;
@@ -16,7 +18,7 @@ export async function startServer(browserFiles) {
16
18
  try {
17
19
  let server = http.createServer((req, res) => {
18
20
  handle(req, res).catch((error) => {
19
- console.error(`[remix-test] Unhandled error for ${req.url}:`, error);
21
+ logError(`Unhandled error for ${req.url}`, error);
20
22
  if (!res.headersSent) {
21
23
  res.writeHead(500, { 'Content-Type': 'text/plain' });
22
24
  }
@@ -28,7 +30,7 @@ export async function startServer(browserFiles) {
28
30
  server.once('error', reject);
29
31
  server.listen(port, () => {
30
32
  server.removeListener('error', reject);
31
- console.log(`Test server running on http://localhost:${port}`);
33
+ log(`Test server running on http://localhost:${port}`);
32
34
  resolve();
33
35
  });
34
36
  });
@@ -38,7 +40,7 @@ export async function startServer(browserFiles) {
38
40
  if (error.code !== 'EADDRINUSE')
39
41
  throw error;
40
42
  lastError = error;
41
- console.log(`Port ${port} is in use, trying another port...`);
43
+ log(`Port ${port} is in use, trying another port...`);
42
44
  port += 1;
43
45
  }
44
46
  }
@@ -88,7 +90,7 @@ function createRequestHandler(browserFiles) {
88
90
  return;
89
91
  }
90
92
  catch (error) {
91
- console.error(`[remix-test] Error serving ${url.pathname}:`, error);
93
+ logError(`Error serving ${url.pathname}`, error);
92
94
  sendText(res, 500, String(error));
93
95
  return;
94
96
  }
@@ -134,9 +136,8 @@ async function serveScript(res, filePath, urlPath, rootDir) {
134
136
  code = result.code;
135
137
  }
136
138
  catch (error) {
137
- let msg = error instanceof Error ? error.message : String(error);
138
- console.error(`[remix-test] Failed to transform ${urlPath}: ${msg}`);
139
- sendText(res, 500, msg);
139
+ logError(`Failed to transform ${urlPath}`, error);
140
+ sendText(res, 500, `Failed to transform ${urlPath}`);
140
141
  return;
141
142
  }
142
143
  }
@@ -147,9 +148,8 @@ async function serveScript(res, filePath, urlPath, rootDir) {
147
148
  code = await rewriteImports(code, filePath, rootDir);
148
149
  }
149
150
  catch (error) {
150
- let msg = error instanceof Error ? error.message : String(error);
151
- console.error(`[remix-test] Failed to rewrite imports for ${urlPath}: ${msg}`);
152
- sendText(res, 500, msg);
151
+ logError(`Failed to rewrite imports for ${urlPath}`, error);
152
+ sendText(res, 500, `Failed to rewrite imports for ${urlPath}`);
153
153
  return;
154
154
  }
155
155
  res.writeHead(200, { 'Content-Type': 'application/javascript' });
package/dist/cli.d.ts CHANGED
@@ -1,8 +1,38 @@
1
1
  import { getRemixTestHelpText } from './lib/config.ts';
2
2
  export { getRemixTestHelpText };
3
+ /**
4
+ * Options accepted by {@link runRemixTest}.
5
+ */
3
6
  export interface RunRemixTestOptions {
7
+ /**
8
+ * Argument vector to parse. When omitted, `process.argv.slice(2)` is used
9
+ * so the regular CLI flags work transparently.
10
+ */
4
11
  argv?: string[];
12
+ /**
13
+ * Working directory the runner resolves config and test files against
14
+ * (default `process.cwd()`).
15
+ */
5
16
  cwd?: string;
6
17
  }
18
+ /**
19
+ * Programmatic entry point for the `remix-test` CLI. Loads the user's
20
+ * {@link RemixTestConfig}, discovers test files, and runs them through the
21
+ * server/browser/E2E pipelines configured by the project. In watch mode the
22
+ * promise resolves when the user terminates the runner; otherwise it resolves
23
+ * once the run finishes.
24
+ *
25
+ * @param options Optional overrides for the parsed argv and working directory.
26
+ * @returns The exit code the host process should use (`0` on success, `1` on
27
+ * test failure or unrecoverable error).
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { runRemixTest } from '@remix-run/test/cli'
32
+ *
33
+ * let exitCode = await runRemixTest()
34
+ * process.exit(exitCode)
35
+ * ```
36
+ */
7
37
  export declare function runRemixTest(options?: RunRemixTestOptions): Promise<number>;
8
38
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,oBAAoB,EAIrB,MAAM,iBAAiB,CAAA;AAYxB,OAAO,EAAE,oBAAoB,EAAE,CAAA;AAE/B,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AASD,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAerF"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,oBAAoB,EAIrB,MAAM,iBAAiB,CAAA;AAWxB,OAAO,EAAE,oBAAoB,EAAE,CAAA;AAK/B;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAWD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAerF"}
package/dist/cli.js CHANGED
@@ -2,15 +2,33 @@ import * as fsp from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { getRemixTestHelpText, IS_RUNNING_FROM_SRC, loadConfig, } from "./lib/config.js";
4
4
  import { generateCombinedCoverageReport } from "./lib/coverage.js";
5
- import { loadPlaywrightConfig, resolveProjects } from "./lib/playwright.js";
6
5
  import { createReporter } from "./lib/reporters/index.js";
7
- import { runBrowserTests } from "./lib/runner-browser.js";
8
6
  import { runServerTests } from "./lib/runner.js";
9
7
  import { createWatcher } from "./lib/watcher.js";
10
8
  import { importModule } from "./lib/import-module.js";
11
9
  import { IS_BUN } from "./lib/runtime.js";
12
10
  import { isMainThread } from 'node:worker_threads';
13
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
+ /**
14
+ * Programmatic entry point for the `remix-test` CLI. Loads the user's
15
+ * {@link RemixTestConfig}, discovers test files, and runs them through the
16
+ * server/browser/E2E pipelines configured by the project. In watch mode the
17
+ * promise resolves when the user terminates the runner; otherwise it resolves
18
+ * once the run finishes.
19
+ *
20
+ * @param options Optional overrides for the parsed argv and working directory.
21
+ * @returns The exit code the host process should use (`0` on success, `1` on
22
+ * test failure or unrecoverable error).
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { runRemixTest } from '@remix-run/test/cli'
27
+ *
28
+ * let exitCode = await runRemixTest()
29
+ * process.exit(exitCode)
30
+ * ```
31
+ */
14
32
  export async function runRemixTest(options = {}) {
15
33
  let argv = options.argv ?? process.argv.slice(2);
16
34
  let cwd = await resolveCwd(options.cwd ?? process.cwd());
@@ -119,9 +137,6 @@ async function runRemixTestInCwd(argv, cwd) {
119
137
  browserServerFilesKey = browserFilesKey;
120
138
  browserPort = result.port;
121
139
  }
122
- let playwrightConfig = config.playwrightConfig == null || typeof config.playwrightConfig === 'string'
123
- ? await loadPlaywrightConfig(config.playwrightConfig, cwd)
124
- : config.playwrightConfig;
125
140
  let reporter = createReporter(config.reporter);
126
141
  let startTime = performance.now();
127
142
  let counts = {
@@ -136,6 +151,7 @@ async function runRemixTestInCwd(argv, cwd) {
136
151
  let serverResult = await runServerTests(serverFiles, reporter, config.concurrency, 'server', {
137
152
  coverage: config.coverage,
138
153
  cwd,
154
+ pool: config.pool,
139
155
  });
140
156
  counts.failed += serverResult.failed;
141
157
  counts.passed += serverResult.passed;
@@ -145,12 +161,17 @@ async function runRemixTestInCwd(argv, cwd) {
145
161
  }
146
162
  // Run browser/e2e tests for all browsers configured by the user
147
163
  if (browserFiles.length > 0 || e2eFiles.length > 0) {
164
+ let { loadPlaywrightConfig, resolveProjects } = await importPlaywrightSupport();
165
+ let runBrowserTests = browserFiles.length > 0 ? (await importBrowserTestRunner()).runBrowserTests : undefined;
166
+ let playwrightConfig = config.playwrightConfig == null || typeof config.playwrightConfig === 'string'
167
+ ? await loadPlaywrightConfig(config.playwrightConfig, cwd)
168
+ : config.playwrightConfig;
148
169
  let projects = resolveProjects(playwrightConfig);
149
170
  if (config.project) {
150
- let projectNames = config.project.split(',').map((project) => project.trim());
151
- projects = projects.filter((project) => project.name && projectNames.includes(project.name));
171
+ let projectNames = new Set(config.project);
172
+ projects = projects.filter((project) => project.name && projectNames.has(project.name));
152
173
  if (projects.length === 0) {
153
- throw new Error(`No playwright projects found with name(s) "${config.project}"`);
174
+ throw new Error(`No playwright projects found with name(s) "${config.project.join(', ')}"`);
154
175
  }
155
176
  }
156
177
  let lastBrowserResult = null;
@@ -166,7 +187,7 @@ async function runRemixTestInCwd(argv, cwd) {
166
187
  }
167
188
  }
168
189
  let [browserResult, e2eResult] = await Promise.all([
169
- browserFiles.length > 0
190
+ runBrowserTests != null
170
191
  ? runBrowserTests({
171
192
  baseUrl: `http://localhost:${browserPort}`,
172
193
  console: config.browser?.echo,
@@ -185,6 +206,7 @@ async function runRemixTestInCwd(argv, cwd) {
185
206
  projectName: project.name,
186
207
  coverage: config.coverage,
187
208
  cwd,
209
+ pool: config.pool,
188
210
  })
189
211
  : null,
190
212
  ]);
@@ -246,6 +268,36 @@ async function runRemixTestInCwd(argv, cwd) {
246
268
  }
247
269
  return await runPromise;
248
270
  }
271
+ async function importPlaywrightSupport() {
272
+ try {
273
+ return await import("./lib/playwright.js");
274
+ }
275
+ catch (error) {
276
+ throw toPlaywrightImportError(error);
277
+ }
278
+ }
279
+ async function importBrowserTestRunner() {
280
+ try {
281
+ return await import("./lib/runner-browser.js");
282
+ }
283
+ catch (error) {
284
+ throw toPlaywrightImportError(error);
285
+ }
286
+ }
287
+ function toPlaywrightImportError(error) {
288
+ return isMissingPlaywrightImport(error) ? new Error(MISSING_PLAYWRIGHT_MESSAGE) : error;
289
+ }
290
+ function isMissingPlaywrightImport(error) {
291
+ if (!isRecord(error) || typeof error.message !== 'string') {
292
+ return false;
293
+ }
294
+ return ((error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND') &&
295
+ (error.message.includes("Cannot find package 'playwright'") ||
296
+ error.message.includes("Cannot find module 'playwright'")));
297
+ }
298
+ function isRecord(value) {
299
+ return typeof value === 'object' && value !== null;
300
+ }
249
301
  async function resolveCwd(cwd) {
250
302
  try {
251
303
  return await fsp.realpath(cwd);
@@ -257,12 +309,12 @@ async function resolveCwd(cwd) {
257
309
  async function discoverTests(config, cwd) {
258
310
  let files = await findFiles(config.glob.test, config.glob.exclude, cwd);
259
311
  if (files.length === 0) {
260
- console.log(`No test files found matching pattern: ${config.glob.test}`);
312
+ console.log(`No test files found matching pattern: ${config.glob.test.join(', ')}`);
261
313
  return null;
262
314
  }
263
315
  let browserSet = new Set(await findFiles(config.glob.browser, config.glob.exclude, cwd));
264
316
  let e2eSet = new Set(await findFiles(config.glob.e2e, config.glob.exclude, cwd));
265
- let types = new Set(config.type.split(','));
317
+ let types = new Set(config.type);
266
318
  let browserFiles = types.has('browser') ? files.filter((f) => browserSet.has(f)) : [];
267
319
  let e2eFiles = types.has('e2e') ? files.filter((file) => e2eSet.has(file)) : [];
268
320
  let serverFiles = types.has('server')
@@ -270,7 +322,7 @@ async function discoverTests(config, cwd) {
270
322
  : [];
271
323
  let totalFiles = browserFiles.length + serverFiles.length + e2eFiles.length;
272
324
  if (totalFiles === 0) {
273
- console.log(`No test files remain after filtering for type ${config.type}`);
325
+ console.log(`No test files remain after filtering for type ${config.type.join(', ')}`);
274
326
  return null;
275
327
  }
276
328
  console.log(`Found ${totalFiles} test file(s) (${serverFiles.length} server, ${browserFiles.length} browser, ${e2eFiles.length} e2e)`);
@@ -281,25 +333,37 @@ async function discoverTests(config, cwd) {
281
333
  e2eFiles,
282
334
  };
283
335
  }
284
- async function findFiles(pattern, excludePattern, cwd) {
285
- let files = [];
336
+ async function findFiles(patterns, excludePatterns, cwd) {
337
+ let files = new Set();
286
338
  if (IS_BUN) {
287
339
  // Bun's `fs.promises.glob` follows symlinks and doesn't prune traversal
288
340
  // via `exclude`, so it enters pnpm symlink cycles in `node_modules`.
289
341
  // Use Bun's native Glob, which defaults to `followSymlinks: false`.
290
342
  // @ts-expect-error — bun module is only resolvable under the Bun runtime
291
343
  let { Glob } = await import('bun');
292
- let glob = new Glob(pattern);
293
- let excludeGlob = new Glob(excludePattern);
294
- for await (let file of glob.scan({ cwd, absolute: true })) {
295
- if (!excludeGlob.match(path.relative(cwd, file))) {
296
- files.push(file);
344
+ let excludeGlobs = excludePatterns.map((p) => new Glob(p));
345
+ for (let pattern of patterns) {
346
+ let glob = new Glob(pattern);
347
+ for await (let file of glob.scan({ cwd, absolute: true })) {
348
+ let rel = toPosix(path.relative(cwd, file));
349
+ if (!excludeGlobs.some((eg) => eg.match(rel))) {
350
+ files.add(toPosix(file));
351
+ }
297
352
  }
298
353
  }
299
- return files;
354
+ return [...files];
300
355
  }
301
- for await (let file of fsp.glob(pattern, { cwd, exclude: [excludePattern] })) {
302
- files.push(path.resolve(cwd, file));
356
+ for (let pattern of patterns) {
357
+ for await (let file of fsp.glob(pattern, { cwd, exclude: excludePatterns })) {
358
+ files.add(toPosix(path.resolve(cwd, file)));
359
+ }
303
360
  }
304
- return files;
361
+ return [...files];
362
+ }
363
+ // Normalize discovered paths so set membership across the test/browser/e2e
364
+ // `findFiles` calls is byte-stable on every platform. Node accepts forward
365
+ // slashes for filesystem operations on Windows, so downstream `fs.readFile`
366
+ // etc. work without further conversion.
367
+ function toPosix(p) {
368
+ return path.sep === '/' ? p : p.replace(/\\/g, '/');
305
369
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
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';
@@ -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;AACnD,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,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"}
@@ -1,6 +1,18 @@
1
1
  import type { PlaywrightTestConfig } from 'playwright/test';
2
2
  export declare const IS_RUNNING_FROM_SRC: boolean;
3
3
  export declare function getBrowserTestRootDir(): string;
4
+ /**
5
+ * Worker pool used by `remix-test` to run server and E2E test files.
6
+ * `'forks'` (default) uses child processes for stronger isolation; `'threads'`
7
+ * uses worker threads for projects that prefer lower-overhead startup.
8
+ */
9
+ export type RemixTestPool = 'forks' | 'threads';
10
+ /**
11
+ * User-facing configuration for the `remix-test` CLI. Every field is
12
+ * optional — unset fields fall back to runner defaults. The same shape can
13
+ * be exported from a config file (see `--config`) or passed inline to
14
+ * {@link runRemixTest} via the corresponding flags.
15
+ */
4
16
  export interface RemixTestConfig {
5
17
  /**
6
18
  * Options for controlling the playwright browser
@@ -12,17 +24,18 @@ export interface RemixTestConfig {
12
24
  open?: boolean;
13
25
  };
14
26
  /**
15
- * Glob patterns to identify test files
16
- * - `glob.test`: Glob pattern for all test files (--glob.test)
17
- * - `glob.browser`: Glob pattern for the subset of browser test files (--glob.browser)
18
- * - `glob.e2e`: Glob pattern for the subset of e2e test files (--glob.e2e)
19
- * - `glob.exclude`: Glob pattern for paths to exclude from discovery (--glob.exclude)
27
+ * Glob patterns to identify test files. Each field accepts a single pattern
28
+ * or an array of patterns; arrays are unioned during discovery.
29
+ * - `glob.test`: Glob pattern(s) for all test files (--glob.test)
30
+ * - `glob.browser`: Glob pattern(s) for the subset of browser test files (--glob.browser)
31
+ * - `glob.e2e`: Glob pattern(s) for the subset of e2e test files (--glob.e2e)
32
+ * - `glob.exclude`: Glob pattern(s) for paths to exclude from discovery (--glob.exclude)
20
33
  */
21
34
  glob?: {
22
- test?: string;
23
- browser?: string;
24
- e2e?: string;
25
- exclude?: string;
35
+ test?: string | string[];
36
+ browser?: string | string[];
37
+ e2e?: string | string[];
38
+ exclude?: string | string[];
26
39
  };
27
40
  /** Max number of concurrent test workers (--concurrency) */
28
41
  concurrency?: number | string;
@@ -32,8 +45,8 @@ export interface RemixTestConfig {
32
45
  */
33
46
  coverage?: boolean | {
34
47
  dir?: string;
35
- include?: string[];
36
- exclude?: string[];
48
+ include?: string | string[];
49
+ exclude?: string | string[];
37
50
  statements?: number | string;
38
51
  lines?: number | string;
39
52
  branches?: number | string;
@@ -49,12 +62,23 @@ export interface RemixTestConfig {
49
62
  * PlaywrightTestConfig object. CLI `--playwrightConfig` only accepts a file path.
50
63
  */
51
64
  playwrightConfig?: string | PlaywrightTestConfig;
52
- /** Filter tests to a specific playwright project or comma-separated list of projects (--project) */
53
- project?: string;
65
+ /**
66
+ * Pool used to run server and E2E test files. Forked child processes are the default,
67
+ * but worker threads are available for projects that prefer the previous behavior.
68
+ */
69
+ pool?: RemixTestPool;
70
+ /**
71
+ * Filter tests to specific playwright project(s) (--project). Accepts a single
72
+ * project name or an array of names; `--project` may be repeated on the CLI.
73
+ */
74
+ project?: string | string[];
54
75
  /** Test reporter (--reporter) */
55
76
  reporter?: string;
56
- /** Comma-separated list of test types to run (--type) */
57
- type?: string;
77
+ /**
78
+ * Test type(s) to run (--type). Accepts a single type or an array of types;
79
+ * `--type` may be repeated on the CLI. Valid values: "server", "browser", "e2e".
80
+ */
81
+ type?: string | string[];
58
82
  /** Watch mode — re-run tests on file changes (--watch) */
59
83
  watch?: boolean;
60
84
  }
@@ -74,18 +98,28 @@ export interface ResolvedRemixTestConfig {
74
98
  functions?: number;
75
99
  } | undefined;
76
100
  glob: {
77
- test: string;
78
- browser: string;
79
- e2e: string;
80
- exclude: string;
101
+ test: string[];
102
+ browser: string[];
103
+ e2e: string[];
104
+ exclude: string[];
81
105
  };
82
106
  playwrightConfig: string | PlaywrightTestConfig | undefined;
83
- project: string | undefined;
107
+ project: string[] | undefined;
84
108
  reporter: string;
109
+ pool: RemixTestPool;
85
110
  setup: string | undefined;
86
- type: string;
111
+ type: string[];
87
112
  watch: boolean;
88
113
  }
89
114
  export declare function loadConfig(args?: string[], cwd?: string): Promise<ResolvedRemixTestConfig>;
115
+ /**
116
+ * Returns the formatted `remix-test --help` text. Useful for embedding the
117
+ * runner's CLI options in higher-level tooling.
118
+ *
119
+ * @param _target Output stream the help text will be written to. Reserved
120
+ * for future use (e.g. width-aware formatting); currently
121
+ * unused.
122
+ * @returns The help text as a single string ready to write to a stream.
123
+ */
90
124
  export declare function getRemixTestHelpText(_target?: NodeJS.WriteStream): string;
91
125
  //# 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;AAG3D,eAAO,MAAM,mBAAmB,SAA4D,CAAA;AAa5F,wBAAgB,qBAAqB,IAAI,MAAM,CAS9C;AAqID,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;KACf,CAAA;IACD;;;;;;OAMG;IACH,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EACL,OAAO,GACP;QACE,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;QAClB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;QAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;QACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;QAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAC5B,CAAA;IACL;;;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,QAAQ,EACJ;QACE,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;QAClB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,GACD,SAAS,CAAA;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,MAAM,CAAA;KAChB,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,CAAC,IAAI,GAAE,MAAM,EAA0B,EAAE,GAAG,SAAgB,oCAK3F;AAED,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAmBzF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}