@remix-run/test 0.2.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 (60) hide show
  1. package/README.md +39 -33
  2. package/dist/app/client/entry.js +4 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +68 -23
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/lib/config.d.ts +35 -21
  8. package/dist/lib/config.d.ts.map +1 -1
  9. package/dist/lib/config.js +73 -33
  10. package/dist/lib/fake-timers.d.ts +7 -0
  11. package/dist/lib/fake-timers.d.ts.map +1 -1
  12. package/dist/lib/fake-timers.js +27 -8
  13. package/dist/lib/import-module.d.ts.map +1 -1
  14. package/dist/lib/import-module.js +11 -2
  15. package/dist/lib/reporters/dot.d.ts.map +1 -1
  16. package/dist/lib/reporters/dot.js +10 -0
  17. package/dist/lib/reporters/files.d.ts.map +1 -1
  18. package/dist/lib/reporters/files.js +10 -0
  19. package/dist/lib/reporters/spec.d.ts.map +1 -1
  20. package/dist/lib/reporters/spec.js +10 -0
  21. package/dist/lib/reporters/tap.d.ts.map +1 -1
  22. package/dist/lib/reporters/tap.js +10 -0
  23. package/dist/lib/runner-browser.d.ts.map +1 -1
  24. package/dist/lib/runner-browser.js +6 -0
  25. package/dist/lib/runner.d.ts +18 -1
  26. package/dist/lib/runner.d.ts.map +1 -1
  27. package/dist/lib/runner.js +187 -38
  28. package/dist/lib/worker-e2e-file.d.ts +11 -0
  29. package/dist/lib/worker-e2e-file.d.ts.map +1 -0
  30. package/dist/lib/worker-e2e-file.js +69 -0
  31. package/dist/lib/worker-e2e.js +11 -47
  32. package/dist/lib/worker-process.d.ts +2 -0
  33. package/dist/lib/worker-process.d.ts.map +1 -0
  34. package/dist/lib/worker-process.js +55 -0
  35. package/dist/lib/worker-results.d.ts +3 -0
  36. package/dist/lib/worker-results.d.ts.map +1 -0
  37. package/dist/lib/worker-results.js +20 -0
  38. package/dist/lib/worker-server.d.ts +10 -0
  39. package/dist/lib/worker-server.d.ts.map +1 -0
  40. package/dist/lib/worker-server.js +113 -0
  41. package/dist/lib/worker.js +6 -55
  42. package/package.json +4 -4
  43. package/src/app/client/entry.ts +4 -0
  44. package/src/cli.ts +91 -28
  45. package/src/index.ts +1 -1
  46. package/src/lib/config.ts +124 -58
  47. package/src/lib/fake-timers.ts +33 -8
  48. package/src/lib/import-module.ts +12 -2
  49. package/src/lib/reporters/dot.ts +9 -0
  50. package/src/lib/reporters/files.ts +9 -0
  51. package/src/lib/reporters/spec.ts +9 -0
  52. package/src/lib/reporters/tap.ts +9 -0
  53. package/src/lib/runner-browser.ts +6 -0
  54. package/src/lib/runner.ts +253 -50
  55. package/src/lib/worker-e2e-file.ts +98 -0
  56. package/src/lib/worker-e2e.ts +14 -51
  57. package/src/lib/worker-process.ts +69 -0
  58. package/src/lib/worker-results.ts +22 -0
  59. package/src/lib/worker-server.ts +123 -0
  60. package/src/lib/worker.ts +7 -47
package/README.md CHANGED
@@ -41,13 +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
+ 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.
51
52
 
52
53
  If you install `@remix-run/test` directly instead of the umbrella `remix` package, the same runner is available as `remix-test`:
53
54
 
@@ -76,15 +77,18 @@ export default {
76
77
  // Max number of concurrent test workers (default `os.availableParallelism()`)
77
78
  concurrency: 2,
78
79
 
80
+ // Pool for server and E2E test files ("forks", "threads")
81
+ pool: 'forks',
82
+
79
83
  // Code coverage options
80
84
  coverage: {
81
85
  // Enable coverage reporting
82
86
  enabled: true,
83
87
  // Output directory (default: ".coverage")
84
88
  dir: '.coverage',
85
- // Glob patterns to include/exclude
86
- include: ['src/**'],
87
- exclude: ['src/**/*.test.ts'],
89
+ // Glob pattern(s) to include/exclude
90
+ include: 'src/**',
91
+ exclude: 'src/**/*.test.ts',
88
92
  // Minimum thresholds (%)
89
93
  statements: 80,
90
94
  lines: 80,
@@ -92,12 +96,13 @@ export default {
92
96
  functions: 80,
93
97
  },
94
98
 
99
+ // Glob pattern(s) identifying test files
95
100
  glob: {
96
- // Glob pattern identifying all test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}")
101
+ // All test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}").
97
102
  test: '**/*.test{,.browser,.e2e}.ts',
98
- // Glob pattern identifying browser test files (default: "**/*.test.browser.{ts,tsx}")
103
+ // Browser test files (default: "**/*.test.browser.{ts,tsx}")
99
104
  browser: '**/*.test.browser.ts',
100
- // Glob pattern identifying E2E test files (default: "**/*.test.e2e.{ts,tsx}")
105
+ // E2E test files (default: "**/*.test.e2e.{ts,tsx}")
101
106
  e2e: '**/*.test.e2e.ts',
102
107
  },
103
108
 
@@ -114,7 +119,7 @@ export default {
114
119
  },
115
120
  },
116
121
 
117
- // Comma-separated list of playwright projects to run E2E tests for
122
+ // Playwright project(s) to run E2E tests for
118
123
  project: 'chromium',
119
124
 
120
125
  // Test reporter ("spec", "files", "tap", "dot")
@@ -123,8 +128,8 @@ export default {
123
128
  // Path to a setup module (see Setup section below)
124
129
  setup: './test/setup.ts',
125
130
 
126
- // Comma-separated list of test types to run ("server", "browser", "e2e")
127
- type: 'server,browser,e2e',
131
+ // Test type(s) to run ("server", "browser", "e2e")
132
+ type: ['server', 'browser', 'e2e'],
128
133
 
129
134
  // Watch for file changes and re-run
130
135
  watch: false,
@@ -141,28 +146,29 @@ remix test --config ./tests/config.ts
141
146
 
142
147
  You may also specify any config field as a CLI flag which will take precedence over config file values:
143
148
 
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` |
149
+ | Flag | Short |
150
+ | --------------------------- | --------- | --- |
151
+ | `--browser.echo` | |
152
+ | `--browser.open` | |
153
+ | `--concurrency <n>` | `-c` |
154
+ | `--coverage` | |
155
+ | `--coverage.dir <path>` | |
156
+ | `--coverage.include` | |
157
+ | `--coverage.exclude` | |
158
+ | `--coverage.statements` | |
159
+ | `--coverage.lines` | |
160
+ | `--coverage.branches` | |
161
+ | `--coverage.functions` | |
162
+ | `--glob.test` | |
163
+ | `--glob.browser` | |
164
+ | `--glob.e2e` | |
165
+ | `--playwrightConfig <path>` | |
166
+ | `--pool <forks | threads>` | |
167
+ | `--project <name>` | `-p` |
168
+ | `--reporter <name>` | `-r` |
169
+ | `--setup <path>` | `-s` |
170
+ | `--type <name>` | `-t` |
171
+ | `--watch` | `-w` |
166
172
 
167
173
  ### Setup
168
174
 
@@ -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)
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,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAWD,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAerF"}
package/dist/cli.js CHANGED
@@ -2,15 +2,14 @@ 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`.';
14
13
  export async function runRemixTest(options = {}) {
15
14
  let argv = options.argv ?? process.argv.slice(2);
16
15
  let cwd = await resolveCwd(options.cwd ?? process.cwd());
@@ -119,9 +118,6 @@ async function runRemixTestInCwd(argv, cwd) {
119
118
  browserServerFilesKey = browserFilesKey;
120
119
  browserPort = result.port;
121
120
  }
122
- let playwrightConfig = config.playwrightConfig == null || typeof config.playwrightConfig === 'string'
123
- ? await loadPlaywrightConfig(config.playwrightConfig, cwd)
124
- : config.playwrightConfig;
125
121
  let reporter = createReporter(config.reporter);
126
122
  let startTime = performance.now();
127
123
  let counts = {
@@ -136,6 +132,7 @@ async function runRemixTestInCwd(argv, cwd) {
136
132
  let serverResult = await runServerTests(serverFiles, reporter, config.concurrency, 'server', {
137
133
  coverage: config.coverage,
138
134
  cwd,
135
+ pool: config.pool,
139
136
  });
140
137
  counts.failed += serverResult.failed;
141
138
  counts.passed += serverResult.passed;
@@ -145,12 +142,17 @@ async function runRemixTestInCwd(argv, cwd) {
145
142
  }
146
143
  // Run browser/e2e tests for all browsers configured by the user
147
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;
148
150
  let projects = resolveProjects(playwrightConfig);
149
151
  if (config.project) {
150
- let projectNames = config.project.split(',').map((project) => project.trim());
151
- projects = projects.filter((project) => project.name && projectNames.includes(project.name));
152
+ let projectNames = new Set(config.project);
153
+ projects = projects.filter((project) => project.name && projectNames.has(project.name));
152
154
  if (projects.length === 0) {
153
- throw new Error(`No playwright projects found with name(s) "${config.project}"`);
155
+ throw new Error(`No playwright projects found with name(s) "${config.project.join(', ')}"`);
154
156
  }
155
157
  }
156
158
  let lastBrowserResult = null;
@@ -166,7 +168,7 @@ async function runRemixTestInCwd(argv, cwd) {
166
168
  }
167
169
  }
168
170
  let [browserResult, e2eResult] = await Promise.all([
169
- browserFiles.length > 0
171
+ runBrowserTests != null
170
172
  ? runBrowserTests({
171
173
  baseUrl: `http://localhost:${browserPort}`,
172
174
  console: config.browser?.echo,
@@ -185,6 +187,7 @@ async function runRemixTestInCwd(argv, cwd) {
185
187
  projectName: project.name,
186
188
  coverage: config.coverage,
187
189
  cwd,
190
+ pool: config.pool,
188
191
  })
189
192
  : null,
190
193
  ]);
@@ -246,6 +249,36 @@ async function runRemixTestInCwd(argv, cwd) {
246
249
  }
247
250
  return await runPromise;
248
251
  }
252
+ async function importPlaywrightSupport() {
253
+ try {
254
+ return await import("./lib/playwright.js");
255
+ }
256
+ catch (error) {
257
+ throw toPlaywrightImportError(error);
258
+ }
259
+ }
260
+ async function importBrowserTestRunner() {
261
+ try {
262
+ return await import("./lib/runner-browser.js");
263
+ }
264
+ catch (error) {
265
+ throw toPlaywrightImportError(error);
266
+ }
267
+ }
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
+ }
249
282
  async function resolveCwd(cwd) {
250
283
  try {
251
284
  return await fsp.realpath(cwd);
@@ -257,12 +290,12 @@ async function resolveCwd(cwd) {
257
290
  async function discoverTests(config, cwd) {
258
291
  let files = await findFiles(config.glob.test, config.glob.exclude, cwd);
259
292
  if (files.length === 0) {
260
- console.log(`No test files found matching pattern: ${config.glob.test}`);
293
+ console.log(`No test files found matching pattern: ${config.glob.test.join(', ')}`);
261
294
  return null;
262
295
  }
263
296
  let browserSet = new Set(await findFiles(config.glob.browser, config.glob.exclude, cwd));
264
297
  let e2eSet = new Set(await findFiles(config.glob.e2e, config.glob.exclude, cwd));
265
- let types = new Set(config.type.split(','));
298
+ let types = new Set(config.type);
266
299
  let browserFiles = types.has('browser') ? files.filter((f) => browserSet.has(f)) : [];
267
300
  let e2eFiles = types.has('e2e') ? files.filter((file) => e2eSet.has(file)) : [];
268
301
  let serverFiles = types.has('server')
@@ -270,7 +303,7 @@ async function discoverTests(config, cwd) {
270
303
  : [];
271
304
  let totalFiles = browserFiles.length + serverFiles.length + e2eFiles.length;
272
305
  if (totalFiles === 0) {
273
- console.log(`No test files remain after filtering for type ${config.type}`);
306
+ console.log(`No test files remain after filtering for type ${config.type.join(', ')}`);
274
307
  return null;
275
308
  }
276
309
  console.log(`Found ${totalFiles} test file(s) (${serverFiles.length} server, ${browserFiles.length} browser, ${e2eFiles.length} e2e)`);
@@ -281,25 +314,37 @@ async function discoverTests(config, cwd) {
281
314
  e2eFiles,
282
315
  };
283
316
  }
284
- async function findFiles(pattern, excludePattern, cwd) {
285
- let files = [];
317
+ async function findFiles(patterns, excludePatterns, cwd) {
318
+ let files = new Set();
286
319
  if (IS_BUN) {
287
320
  // Bun's `fs.promises.glob` follows symlinks and doesn't prune traversal
288
321
  // via `exclude`, so it enters pnpm symlink cycles in `node_modules`.
289
322
  // Use Bun's native Glob, which defaults to `followSymlinks: false`.
290
323
  // @ts-expect-error — bun module is only resolvable under the Bun runtime
291
324
  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);
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
+ }
297
333
  }
298
334
  }
299
- return files;
335
+ return [...files];
300
336
  }
301
- for await (let file of fsp.glob(pattern, { cwd, exclude: [excludePattern] })) {
302
- files.push(path.resolve(cwd, file));
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)));
340
+ }
303
341
  }
304
- return files;
342
+ return [...files];
343
+ }
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, '/');
305
350
  }
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,7 @@
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
+ export type RemixTestPool = 'forks' | 'threads';
4
5
  export interface RemixTestConfig {
5
6
  /**
6
7
  * Options for controlling the playwright browser
@@ -12,17 +13,18 @@ export interface RemixTestConfig {
12
13
  open?: boolean;
13
14
  };
14
15
  /**
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)
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)
20
22
  */
21
23
  glob?: {
22
- test?: string;
23
- browser?: string;
24
- e2e?: string;
25
- exclude?: string;
24
+ test?: string | string[];
25
+ browser?: string | string[];
26
+ e2e?: string | string[];
27
+ exclude?: string | string[];
26
28
  };
27
29
  /** Max number of concurrent test workers (--concurrency) */
28
30
  concurrency?: number | string;
@@ -32,8 +34,8 @@ export interface RemixTestConfig {
32
34
  */
33
35
  coverage?: boolean | {
34
36
  dir?: string;
35
- include?: string[];
36
- exclude?: string[];
37
+ include?: string | string[];
38
+ exclude?: string | string[];
37
39
  statements?: number | string;
38
40
  lines?: number | string;
39
41
  branches?: number | string;
@@ -49,12 +51,23 @@ export interface RemixTestConfig {
49
51
  * PlaywrightTestConfig object. CLI `--playwrightConfig` only accepts a file path.
50
52
  */
51
53
  playwrightConfig?: string | PlaywrightTestConfig;
52
- /** Filter tests to a specific playwright project or comma-separated list of projects (--project) */
53
- 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[];
54
64
  /** Test reporter (--reporter) */
55
65
  reporter?: string;
56
- /** Comma-separated list of test types to run (--type) */
57
- 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[];
58
71
  /** Watch mode — re-run tests on file changes (--watch) */
59
72
  watch?: boolean;
60
73
  }
@@ -74,16 +87,17 @@ export interface ResolvedRemixTestConfig {
74
87
  functions?: number;
75
88
  } | undefined;
76
89
  glob: {
77
- test: string;
78
- browser: string;
79
- e2e: string;
80
- exclude: string;
90
+ test: string[];
91
+ browser: string[];
92
+ e2e: string[];
93
+ exclude: string[];
81
94
  };
82
95
  playwrightConfig: string | PlaywrightTestConfig | undefined;
83
- project: string | undefined;
96
+ project: string[] | undefined;
84
97
  reporter: string;
98
+ pool: RemixTestPool;
85
99
  setup: string | undefined;
86
- type: string;
100
+ type: string[];
87
101
  watch: boolean;
88
102
  }
89
103
  export declare function loadConfig(args?: string[], cwd?: string): Promise<ResolvedRemixTestConfig>;
@@ -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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"}
@@ -40,19 +40,23 @@ const cliOptions = {
40
40
  },
41
41
  'glob.browser': {
42
42
  type: 'string',
43
- description: 'Glob pattern for browser test files',
43
+ multiple: true,
44
+ description: 'Glob pattern(s) for browser test files',
44
45
  },
45
46
  'glob.e2e': {
46
47
  type: 'string',
47
- description: 'Glob pattern for E2E test files',
48
+ multiple: true,
49
+ description: 'Glob pattern(s) for E2E test files',
48
50
  },
49
51
  'glob.exclude': {
50
52
  type: 'string',
51
- description: 'Glob pattern for paths to exclude from discovery',
53
+ multiple: true,
54
+ description: 'Glob pattern(s) for paths to exclude from discovery',
52
55
  },
53
56
  'glob.test': {
54
57
  type: 'string',
55
- description: 'Glob pattern for all test files',
58
+ multiple: true,
59
+ description: 'Glob pattern(s) for all test files',
56
60
  },
57
61
  concurrency: {
58
62
  type: 'string',
@@ -109,7 +113,12 @@ const cliOptions = {
109
113
  project: {
110
114
  type: 'string',
111
115
  short: 'p',
112
- 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)',
113
122
  },
114
123
  reporter: {
115
124
  type: 'string',
@@ -119,7 +128,8 @@ const cliOptions = {
119
128
  type: {
120
129
  type: 'string',
121
130
  short: 't',
122
- description: 'Comma-separated test types to run (default: server,browser,e2e)',
131
+ multiple: true,
132
+ description: 'Test types to run (default: server, browser, e2e)',
123
133
  },
124
134
  watch: {
125
135
  type: 'boolean',
@@ -143,16 +153,17 @@ const defaultValues = {
143
153
  functions: undefined,
144
154
  },
145
155
  glob: {
146
- test: '**/*.test{,.e2e,.browser}.{ts,tsx}',
147
- browser: '**/*.test.browser.{ts,tsx}',
148
- e2e: '**/*.test.e2e.{ts,tsx}',
149
- exclude: 'node_modules/**',
156
+ test: ['**/*.test{,.e2e,.browser}.{ts,tsx}'],
157
+ browser: ['**/*.test.browser.{ts,tsx}'],
158
+ e2e: ['**/*.test.e2e.{ts,tsx}'],
159
+ exclude: ['node_modules/**'],
150
160
  },
151
- reporter: process.env.CI === 'true' ? 'files' : 'spec',
152
- type: 'server,browser,e2e',
153
- setup: undefined,
161
+ pool: 'forks',
154
162
  playwrightConfig: undefined,
155
163
  project: undefined,
164
+ reporter: process.env.CI === 'true' ? 'files' : 'spec',
165
+ setup: undefined,
166
+ type: ['server', 'browser', 'e2e'],
156
167
  watch: false,
157
168
  };
158
169
  export async function loadConfig(args = process.argv.slice(2), cwd = process.cwd()) {
@@ -163,10 +174,10 @@ export async function loadConfig(args = process.argv.slice(2), cwd = process.cwd
163
174
  }
164
175
  export function getRemixTestHelpText(_target = process.stdout) {
165
176
  let lines = [
166
- 'Usage: remix-test [glob] [options]',
177
+ 'Usage: remix-test [glob...] [options]',
167
178
  '',
168
179
  'Arguments:',
169
- ` glob Glob pattern for test files (default: "${defaultValues.glob.test}")`,
180
+ ` glob Glob pattern(s) for test files (default: "${defaultValues.glob.test.join(', ')}")`,
170
181
  '',
171
182
  'Options:',
172
183
  ];
@@ -181,17 +192,25 @@ export function getRemixTestHelpText(_target = process.stdout) {
181
192
  function parseCliArgs(args) {
182
193
  return util.parseArgs({ args, options: cliOptions, allowPositionals: true });
183
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
+ }
184
204
  function resolveConfig(fileConfig, { values: cliValues, positionals }) {
185
205
  let fileCoverage = typeof fileConfig.coverage === 'boolean' ? {} : fileConfig.coverage || {};
186
206
  return {
187
207
  glob: {
188
- test: positionals[0] ??
189
- cliValues['glob.test'] ??
190
- fileConfig.glob?.test ??
191
- defaultValues.glob.test,
192
- browser: cliValues['glob.browser'] ?? fileConfig.glob?.browser ?? defaultValues.glob.browser,
193
- e2e: cliValues['glob.e2e'] ?? fileConfig.glob?.e2e ?? defaultValues.glob.e2e,
194
- exclude: cliValues['glob.exclude'] ?? fileConfig.glob?.exclude ?? defaultValues.glob.exclude,
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),
195
214
  },
196
215
  browser: {
197
216
  echo: cliValues['browser.echo'] ?? fileConfig.browser?.echo ?? defaultValues.browser.echo,
@@ -201,12 +220,18 @@ function resolveConfig(fileConfig, { values: cliValues, positionals }) {
201
220
  coverage: cliValues.coverage === true || !!fileConfig.coverage
202
221
  ? {
203
222
  dir: cliValues['coverage.dir'] ?? fileCoverage.dir ?? defaultValues.coverage.dir,
204
- include: cliValues['coverage.include'] ??
205
- fileCoverage.include ??
206
- defaultValues.coverage.include,
207
- exclude: cliValues['coverage.exclude'] ??
208
- fileCoverage.exclude ??
209
- defaultValues.coverage.exclude,
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
+ })(),
210
235
  statements: cliValues['coverage.statements'] !== undefined
211
236
  ? Number(cliValues['coverage.statements'])
212
237
  : fileCoverage.statements !== undefined
@@ -231,12 +256,22 @@ function resolveConfig(fileConfig, { values: cliValues, positionals }) {
231
256
  : undefined,
232
257
  setup: cliValues.setup ?? fileConfig.setup ?? defaultValues.setup,
233
258
  playwrightConfig: cliValues.playwrightConfig ?? fileConfig.playwrightConfig ?? defaultValues.playwrightConfig,
234
- 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
+ })(),
235
264
  reporter: cliValues.reporter ?? fileConfig.reporter ?? defaultValues.reporter,
236
- type: cliValues.type ?? fileConfig.type ?? defaultValues.type,
265
+ type: toCommaSeparatedArray(cliValues.type ?? fileConfig.type ?? defaultValues.type),
237
266
  watch: cliValues.watch ?? fileConfig.watch ?? defaultValues.watch,
238
267
  };
239
268
  }
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
+ }
240
275
  async function loadConfigFile(configPath, cwd) {
241
276
  let candidates = configPath
242
277
  ? [path.resolve(cwd, configPath)]
@@ -244,12 +279,17 @@ async function loadConfigFile(configPath, cwd) {
244
279
  for (let candidate of candidates) {
245
280
  try {
246
281
  await fsp.access(candidate);
247
- let mod = await importModule(candidate, import.meta);
248
- return mod.default ?? mod;
249
282
  }
250
283
  catch {
251
- // not found or failed to load — try next
284
+ // not found — try the next candidate
285
+ continue;
252
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;
253
293
  }
254
294
  return {};
255
295
  }