@remix-run/test 0.3.0 → 0.4.1

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 (40) hide show
  1. package/README.md +4 -11
  2. package/dist/app/server.d.ts.map +1 -1
  3. package/dist/app/server.js +10 -10
  4. package/dist/cli.d.ts +30 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +19 -0
  7. package/dist/lib/config.d.ts +20 -0
  8. package/dist/lib/config.d.ts.map +1 -1
  9. package/dist/lib/config.js +9 -0
  10. package/dist/lib/context.d.ts +5 -5
  11. package/dist/lib/coverage-loader.js +2 -2
  12. package/dist/lib/coverage.js +1 -1
  13. package/dist/lib/fake-timers.d.ts +32 -0
  14. package/dist/lib/fake-timers.d.ts.map +1 -1
  15. package/dist/lib/framework.d.ts +12 -6
  16. package/dist/lib/framework.d.ts.map +1 -1
  17. package/dist/lib/framework.js +24 -12
  18. package/dist/lib/import-module.d.ts.map +1 -1
  19. package/dist/lib/import-module.js +5 -4
  20. package/dist/lib/reporters/results.d.ts +1 -1
  21. package/dist/lib/reporters/results.d.ts.map +1 -1
  22. package/dist/lib/runner-browser.d.ts.map +1 -1
  23. package/dist/lib/runner-browser.js +40 -8
  24. package/dist/lib/worker-server.js +7 -8
  25. package/dist/test/framework.test.browser.js +2 -2
  26. package/package.json +6 -6
  27. package/src/app/server.ts +11 -10
  28. package/src/cli.ts +30 -0
  29. package/src/lib/config.ts +20 -0
  30. package/src/lib/context.ts +5 -5
  31. package/src/lib/coverage-loader.ts +2 -2
  32. package/src/lib/coverage.ts +1 -1
  33. package/src/lib/fake-timers.ts +32 -0
  34. package/src/lib/framework.ts +53 -36
  35. package/src/lib/import-module.ts +5 -4
  36. package/src/lib/reporters/results.ts +1 -1
  37. package/src/lib/runner-browser.ts +46 -8
  38. package/src/lib/ts-transform.ts +1 -1
  39. package/src/lib/worker-server.ts +8 -8
  40. 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
 
@@ -50,13 +50,6 @@ remix test "src/**/*.test.ts" "tests/**/*.test.tsx"
50
50
 
51
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.
52
52
 
53
- If you install `@remix-run/test` directly instead of the umbrella `remix` package, the same runner is available as `remix-test`:
54
-
55
- ```sh
56
- npm i @remix-run/test
57
- remix-test
58
- ```
59
-
60
53
  ### Config File
61
54
 
62
55
  Create a `remix-test.config.ts` (or `.js`) file at the root of your project (shown with default values):
@@ -216,11 +209,11 @@ suite('My Test Suite', () => {
216
209
 
217
210
  ### Programmatic runner
218
211
 
219
- `@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
220
213
  exiting the current process:
221
214
 
222
215
  ```ts
223
- import { runRemixTest } from '@remix-run/test/cli'
216
+ import { runRemixTest } from 'remix/test/cli'
224
217
 
225
218
  let exitCode = await runRemixTest({
226
219
  argv: ['--type', 'server'],
@@ -228,7 +221,7 @@ let exitCode = await runRemixTest({
228
221
  })
229
222
  ```
230
223
 
231
- `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
232
225
  `process.exit()` with that code when the run finishes so open workers, browsers, or project handles
233
226
  cannot keep the CLI alive.
234
227
 
@@ -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":"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"}
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
@@ -10,6 +10,25 @@ import { IS_BUN } from "./lib/runtime.js";
10
10
  import { isMainThread } from 'node:worker_threads';
11
11
  export { getRemixTestHelpText };
12
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
+ */
13
32
  export async function runRemixTest(options = {}) {
14
33
  let argv = options.argv ?? process.argv.slice(2);
15
34
  let cwd = await resolveCwd(options.cwd ?? process.cwd());
@@ -1,7 +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
+ */
4
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
+ */
5
16
  export interface RemixTestConfig {
6
17
  /**
7
18
  * Options for controlling the playwright browser
@@ -101,5 +112,14 @@ export interface ResolvedRemixTestConfig {
101
112
  watch: boolean;
102
113
  }
103
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
+ */
104
124
  export declare function getRemixTestHelpText(_target?: NodeJS.WriteStream): string;
105
125
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
@@ -172,6 +172,15 @@ export async function loadConfig(args = process.argv.slice(2), cwd = process.cwd
172
172
  let config = resolveConfig(fileConfig, parsed);
173
173
  return config;
174
174
  }
175
+ /**
176
+ * Returns the formatted `remix-test --help` text. Useful for embedding the
177
+ * runner's CLI options in higher-level tooling.
178
+ *
179
+ * @param _target Output stream the help text will be written to. Reserved
180
+ * for future use (e.g. width-aware formatting); currently
181
+ * unused.
182
+ * @returns The help text as a single string ready to write to a stream.
183
+ */
175
184
  export function getRemixTestHelpText(_target = process.stdout) {
176
185
  let lines = [
177
186
  'Usage: remix-test [glob...] [options]',
@@ -13,7 +13,7 @@ export interface TestServer {
13
13
  close(): Promise<void>;
14
14
  }
15
15
  /**
16
- * Test Context providing utilities for testing via remix-test. The context is
16
+ * Test Context providing utilities for testing via `remix-test`. The context is
17
17
  * passed as the first argument to the {@link test}/{@link it} functions.
18
18
  *
19
19
  * @example
@@ -33,8 +33,8 @@ export interface TestContext {
33
33
  */
34
34
  after(fn: () => void): void;
35
35
  /**
36
- * Mock tracker for the current test. Mirrors the shape of Node's
37
- * `t.mock`. Method mocks created here are auto-restored on test completion.
36
+ * Mock tracker for the current test using {@link mock}. Mirrors the shape of Node's
37
+ * `t.mock`. Method mocks created via `t.mock` are auto-restored on test completion.
38
38
  */
39
39
  mock: {
40
40
  /**
@@ -67,8 +67,8 @@ export interface TestContext {
67
67
  /**
68
68
  * Wires a running test server up to a Playwright page so the test can drive
69
69
  * it. The server is closed automatically when the test ends. Pair with
70
- * `createTestServer` from `@remix-run/node-fetch-server/test` (or any other
71
- * source of a `{ baseUrl, close }` handle) to spin up the server first.
70
+ * {@link createTestServer} from `@remix-run/node-fetch-server/test` to spin
71
+ * up the server.
72
72
  *
73
73
  * @param server - The running server the page should target
74
74
  * @returns A `Page` whose `baseURL` is set to `server.baseUrl`.
@@ -3,8 +3,8 @@ import { fileURLToPath } from 'node:url';
3
3
  import { transformTypeScript } from "./ts-transform.js";
4
4
  // Custom ESM loader hook for TypeScript files.
5
5
  //
6
- // Replaces tsx's minified transformation with an un-minified esbuild transform
7
- // that preserves line structure. This ensures V8 coverage byte offsets map
6
+ // Applies an un-minified esbuild transform that preserves line structure.
7
+ // This ensures V8 coverage byte offsets map
8
8
  // cleanly to TypeScript source lines via the inline source map, giving
9
9
  // accurate per-line coverage rather than collapsing multiple statements onto
10
10
  // a single minified line.
@@ -151,7 +151,7 @@ export async function collectServerCoverageMap(coverageDataDir, cwd, testFiles)
151
151
  if (success)
152
152
  converted++;
153
153
  }
154
- catch (e) {
154
+ catch {
155
155
  // Skip files that can't be converted
156
156
  }
157
157
  }
@@ -1,12 +1,44 @@
1
+ /**
2
+ * Handle returned by `mock.timers.enable()` for driving fake timers during a
3
+ * test. While enabled, `setTimeout`, `setInterval`, `clearTimeout`,
4
+ * `clearInterval`, and `Date.now` use the fake clock instead of the real one;
5
+ * timers fire only when the test calls `advance` (or `advanceAsync`).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * it('debounces save calls', (t) => {
10
+ * let timers = t.mock.timers.enable()
11
+ * let save = t.mock.fn()
12
+ * let debounced = debounce(save, 100)
13
+ * debounced(); debounced(); debounced()
14
+ * timers.advance(100)
15
+ * assert.equal(save.mock.calls.length, 1)
16
+ * })
17
+ * ```
18
+ */
1
19
  export interface FakeTimers {
20
+ /**
21
+ * Advance the fake clock by `ms` milliseconds, synchronously firing every
22
+ * timer whose deadline is reached during the advance.
23
+ *
24
+ * @param ms Number of milliseconds to advance.
25
+ */
2
26
  advance(ms: number): void;
3
27
  /**
4
28
  * Like `advance`, but yields to microtasks between each timer firing so
5
29
  * Promise continuations (and any timers they schedule) can settle before
6
30
  * the next firing is processed. Use this when a callback awaits work that
7
31
  * itself depends on the fake clock.
32
+ *
33
+ * @param ms Number of milliseconds to advance.
34
+ * @returns A promise that resolves once all reachable timers have fired.
8
35
  */
9
36
  advanceAsync(ms: number): Promise<void>;
37
+ /**
38
+ * Restore the original timer functions and the real clock. Called
39
+ * automatically after the test finishes; may also be called early to
40
+ * disable fake timers mid-test.
41
+ */
10
42
  restore(): void;
11
43
  }
12
44
  export declare function createFakeTimers(): FakeTimers;
@@ -1 +1 @@
1
- {"version":3,"file":"fake-timers.d.ts","sourceRoot":"","sources":["../../src/lib/fake-timers.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,gBAAgB,IAAI,UAAU,CA0E7C"}
1
+ {"version":3,"file":"fake-timers.d.ts","sourceRoot":"","sources":["../../src/lib/fake-timers.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC;;;;OAIG;IACH,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,gBAAgB,IAAI,UAAU,CA0E7C"}
@@ -1,7 +1,9 @@
1
1
  import type { TestContext } from './context.ts';
2
+ declare function describeImpl(name: string, fn: () => void): void;
3
+ declare function describeImpl(name: string, meta: SuiteMeta, fn: () => void): void;
2
4
  /**
3
- * Groups related tests into a named suite. Suites can be nested snd will be displayed
4
- * as such or joined with ` > ` in reporter output. Lifecycle hooks registered inside
5
+ * Groups related tests into a named suite. Suites can be nested and will be displayed
6
+ * as such in reporter output. Lifecycle hooks registered inside
5
7
  * a `describe` block apply only to tests within that block.
6
8
  *
7
9
  * @example
@@ -15,9 +17,10 @@ import type { TestContext } from './context.ts';
15
17
  * describe.todo('planned suite')
16
18
  *
17
19
  * @param name - The suite name shown in reporter output.
20
+ * @param meta - Suite metadata such as `skip` or `only`.
18
21
  * @param fn - A function that registers the tests and lifecycle hooks in this suite.
19
22
  */
20
- export declare const describe: ((name: string, metaOrFn: SuiteMeta | (() => void), fn?: (() => void) | undefined) => void) & {
23
+ export declare const describe: typeof describeImpl & {
21
24
  skip: (name: string, fn: () => void) => void;
22
25
  only: (name: string, fn: () => void) => void;
23
26
  todo: (name: string) => void;
@@ -31,6 +34,8 @@ type TestMeta = {
31
34
  only?: boolean;
32
35
  };
33
36
  type TestFn = (t: TestContext) => void | Promise<void>;
37
+ declare function itImpl(name: string, fn: TestFn): void;
38
+ declare function itImpl(name: string, meta: TestMeta, fn: TestFn): void;
34
39
  /**
35
40
  * Defines a single test case. The optional `TestContext` argument `t` provides
36
41
  * mock helpers and per-test cleanup registration.
@@ -47,21 +52,22 @@ type TestFn = (t: TestContext) => void | Promise<void>;
47
52
  * it.todo('coming soon')
48
53
  *
49
54
  * @param name - The test name shown in reporter output.
55
+ * @param meta - Test metadata such as `skip` or `only`.
50
56
  * @param fn - The test body, receiving a {@link TestContext} as its first argument.
51
57
  */
52
- export declare const it: ((name: string, metaOrFn: TestFn | TestMeta, fn?: TestFn | undefined) => void) & {
58
+ export declare const it: typeof itImpl & {
53
59
  skip: (name: string, fn?: TestFn | undefined) => void;
54
60
  only: (name: string, fn: TestFn) => void;
55
61
  todo: (name: string) => void;
56
62
  };
57
63
  /** Alias for {@link describe}. */
58
- export declare const suite: ((name: string, metaOrFn: SuiteMeta | (() => void), fn?: (() => void) | undefined) => void) & {
64
+ export declare const suite: typeof describeImpl & {
59
65
  skip: (name: string, fn: () => void) => void;
60
66
  only: (name: string, fn: () => void) => void;
61
67
  todo: (name: string) => void;
62
68
  };
63
69
  /** Alias for {@link it}. */
64
- export declare const test: ((name: string, metaOrFn: TestFn | TestMeta, fn?: TestFn | undefined) => void) & {
70
+ export declare const test: typeof itImpl & {
65
71
  skip: (name: string, fn?: TestFn | undefined) => void;
66
72
  only: (name: string, fn: TestFn) => void;
67
73
  todo: (name: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"framework.d.ts","sourceRoot":"","sources":["../../src/lib/framework.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAkF/C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,QAAQ;;;;CAiBpB,CAAA;AAED,KAAK,SAAS,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AACnD,KAAK,QAAQ,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAClD,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAUtD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,EAAE;;;;CAiBd,CAAA;AAED,kCAAkC;AAClC,eAAO,MAAM,KAAK;;;;CAAW,CAAA;AAC7B,4BAA4B;AAC5B,eAAO,MAAM,IAAI;;;;CAAK,CAAA;AA2BtB;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGxD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGtD;AAED,mEAAiE;AACjE,eAAO,MAAM,MAAM,kBAAY,CAAA;AAC/B,kEAAgE;AAChE,eAAO,MAAM,KAAK,iBAAW,CAAA"}
1
+ {"version":3,"file":"framework.d.ts","sourceRoot":"","sources":["../../src/lib/framework.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AA4F/C,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;AACzD,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;AAO1E;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,QAAQ;;;;CAUnB,CAAA;AAEF,KAAK,SAAS,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AACnD,KAAK,QAAQ,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAClD,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAatD,iBAAS,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;AAC/C,iBAAS,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;AAO/D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,EAAE;;;;CAUb,CAAA;AAEF,kCAAkC;AAClC,eAAO,MAAM,KAAK;;;;CAAW,CAAA;AAC7B,4BAA4B;AAC5B,eAAO,MAAM,IAAI;;;;CAAK,CAAA;AA2BtB;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGxD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAGtD;AAED,mEAAiE;AACjE,eAAO,MAAM,MAAM,kBAAY,CAAA;AAC/B,kEAAgE;AAChE,eAAO,MAAM,KAAK,iBAAW,CAAA"}
@@ -26,6 +26,14 @@ function registerDescribe(name, fn, flags) {
26
26
  throw new Error(`Duplicate suite name: "${fullName}"`);
27
27
  }
28
28
  let suite = { name: fullName, tests: [], ...flags };
29
+ // Children inherit `skip`/`only` from their parent so that
30
+ // `describe.skip('parent', () => describe('child', () => it(...)))` actually
31
+ // skips the child's tests. The executor walks `rootSuites` as a flat list and
32
+ // only inspects each suite's own flag, so the propagation has to happen here.
33
+ if (currentSuite?.skip)
34
+ suite.skip = true;
35
+ if (currentSuite?.only)
36
+ suite.only = true;
29
37
  // Inherit lifecycle hooks from parent suite (or root hooks if at top level)
30
38
  let parent = currentSuite ?? rootHooks;
31
39
  if (parent.beforeEach)
@@ -53,9 +61,14 @@ function registerDescribe(name, fn, flags) {
53
61
  currentSuite = prevSuite;
54
62
  }
55
63
  }
64
+ function describeImpl(name, metaOrFn, fn) {
65
+ let meta = typeof metaOrFn === 'function' ? {} : metaOrFn;
66
+ let suiteFn = typeof metaOrFn === 'function' ? metaOrFn : fn;
67
+ registerDescribe(name, suiteFn, meta);
68
+ }
56
69
  /**
57
- * Groups related tests into a named suite. Suites can be nested snd will be displayed
58
- * as such or joined with ` > ` in reporter output. Lifecycle hooks registered inside
70
+ * Groups related tests into a named suite. Suites can be nested and will be displayed
71
+ * as such in reporter output. Lifecycle hooks registered inside
59
72
  * a `describe` block apply only to tests within that block.
60
73
  *
61
74
  * @example
@@ -69,13 +82,10 @@ function registerDescribe(name, fn, flags) {
69
82
  * describe.todo('planned suite')
70
83
  *
71
84
  * @param name - The suite name shown in reporter output.
85
+ * @param meta - Suite metadata such as `skip` or `only`.
72
86
  * @param fn - A function that registers the tests and lifecycle hooks in this suite.
73
87
  */
74
- export const describe = Object.assign((name, metaOrFn, fn) => {
75
- let meta = typeof metaOrFn === 'function' ? {} : metaOrFn;
76
- let suiteFn = typeof metaOrFn === 'function' ? metaOrFn : fn;
77
- registerDescribe(name, suiteFn, meta);
78
- }, {
88
+ export const describe = Object.assign(describeImpl, {
79
89
  skip: (name, fn) => registerDescribe(name, fn, { skip: true }),
80
90
  only: (name, fn) => registerDescribe(name, fn, { only: true }),
81
91
  todo: (name) => {
@@ -93,6 +103,11 @@ function registerIt(name, fn, flags) {
93
103
  }
94
104
  suite.tests.push({ name, fn, suite, ...flags });
95
105
  }
106
+ function itImpl(name, metaOrFn, fn) {
107
+ let meta = typeof metaOrFn === 'function' ? {} : metaOrFn;
108
+ let testFn = typeof metaOrFn === 'function' ? metaOrFn : fn;
109
+ registerIt(name, testFn, meta);
110
+ }
96
111
  /**
97
112
  * Defines a single test case. The optional `TestContext` argument `t` provides
98
113
  * mock helpers and per-test cleanup registration.
@@ -109,13 +124,10 @@ function registerIt(name, fn, flags) {
109
124
  * it.todo('coming soon')
110
125
  *
111
126
  * @param name - The test name shown in reporter output.
127
+ * @param meta - Test metadata such as `skip` or `only`.
112
128
  * @param fn - The test body, receiving a {@link TestContext} as its first argument.
113
129
  */
114
- export const it = Object.assign((name, metaOrFn, fn) => {
115
- let meta = typeof metaOrFn === 'function' ? {} : metaOrFn;
116
- let testFn = typeof metaOrFn === 'function' ? metaOrFn : fn;
117
- registerIt(name, testFn, meta);
118
- }, {
130
+ export const it = Object.assign(itImpl, {
119
131
  skip: (name, fn) => registerIt(name, fn ?? (() => { }), { skip: true }),
120
132
  only: (name, fn) => registerIt(name, fn, { only: true }),
121
133
  todo: (name) => {
@@ -1 +1 @@
1
- {"version":3,"file":"import-module.d.ts","sourceRoot":"","sources":["../../src/lib/import-module.ts"],"names":[],"mappings":"AAoBA,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAkBpF"}
1
+ {"version":3,"file":"import-module.d.ts","sourceRoot":"","sources":["../../src/lib/import-module.ts"],"names":[],"mappings":"AAmBA,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAoBpF"}
@@ -8,7 +8,6 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import * as path from 'node:path';
10
10
  import { pathToFileURL } from 'node:url';
11
- import { tsImport } from 'tsx/esm/api';
12
11
  import { IS_BUN } from "./runtime.js";
13
12
  function hasImportMetaResolve(meta) {
14
13
  return 'resolve' in meta && typeof meta.resolve === 'function';
@@ -23,8 +22,8 @@ function hasImportMetaResolve(meta) {
23
22
  export async function importModule(specifier, meta) {
24
23
  // Absolute Windows paths (`C:\foo\bar.ts`) aren't valid ESM specifiers — only
25
24
  // `file:///C:/foo/bar.ts` URLs, relative specifiers, or POSIX absolute paths
26
- // are. Convert any absolute filesystem path to its `file:` URL so loaders like
27
- // `tsImport` and `import()` accept it on every platform. POSIX absolute paths
25
+ // are. Convert any absolute filesystem path to its `file:` URL so module
26
+ // loaders and `import()` accept it on every platform. POSIX absolute paths
28
27
  // happen to work as specifiers without conversion, but going through
29
28
  // `pathToFileURL` is safe and platform-agnostic.
30
29
  let resolvedSpecifier = path.isAbsolute(specifier) ? pathToFileURL(specifier).href : specifier;
@@ -34,5 +33,7 @@ export async function importModule(specifier, meta) {
34
33
  }
35
34
  return import(__rewriteRelativeImportExtension(meta.resolve(resolvedSpecifier, meta.url)));
36
35
  }
37
- return tsImport(resolvedSpecifier, meta.url);
36
+ // node-tsx uses Node APIs that fail in Bun if statically imported
37
+ let { loadModule } = await import('@remix-run/node-tsx/load-module');
38
+ return loadModule(resolvedSpecifier, meta.url);
38
39
  }
@@ -1,4 +1,4 @@
1
- import type { V8CoverageEntry } from '../coverage';
1
+ import type { V8CoverageEntry } from '../coverage.ts';
2
2
  export interface TestResult {
3
3
  name: string;
4
4
  suiteName: string;
@@ -1 +1 @@
1
- {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/results.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IAChD,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;IACD,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,yBAAyB,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,eAAe,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACnF;AAED,MAAM,MAAM,MAAM,GAAG;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb,CAAA"}
1
+ {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/results.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IAChD,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;IACD,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,yBAAyB,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,eAAe,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACnF;AAED,MAAM,MAAM,MAAM,GAAG;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"runner-browser.d.ts","sourceRoot":"","sources":["../../src/lib/runner-browser.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAA;AACtB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAWzD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,QAAQ,CAAA;IAGlB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC;IACtE,OAAO,EAAE,WAAW,CAAA;IACpB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAA;IAC/B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B,CAAC,CA6HD"}
1
+ {"version":3,"file":"runner-browser.d.ts","sourceRoot":"","sources":["../../src/lib/runner-browser.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAA;AACtB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAazD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,QAAQ,CAAA;IAGlB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC;IACtE,OAAO,EAAE,WAAW,CAAA;IACpB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAA;IAC/B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B,CAAC,CAiKD"}
@@ -3,6 +3,7 @@ import { colors } from "./colors.js";
3
3
  import { getBrowserTestRootDir } from "./config.js";
4
4
  import { collectCoverageMapFromPlaywright, } from "./coverage.js";
5
5
  import { getBrowserLauncher, getPlaywrightLaunchOptions, getPlaywrightPageOptions, } from "./playwright.js";
6
+ const BROWSER_TEST_FILE_TIMEOUT_MS = 90_000;
6
7
  // The harness reports each test result with `filePath` set to the
7
8
  // `/scripts/<rel>` URL the iframe loaded. Reporters expect a real filesystem
8
9
  // path so they can compute `path.relative(cwd, ...)` cleanly; otherwise they
@@ -27,12 +28,10 @@ export async function runBrowserTests(options) {
27
28
  try {
28
29
  browser = await getBrowserLauncher(options.playwrightUseOpts).launch(getPlaywrightLaunchOptions(options.playwrightUseOpts));
29
30
  page = await browser.newPage(getPlaywrightPageOptions(options.playwrightUseOpts));
30
- // Cap how long we'll wait for a browser-test file to signal completion.
31
- // Playwright's default is 30s; bumping to 60s buys headroom for slower
32
- // suites without letting a hung test hide forever. Plumb this through
33
- // config later if anyone needs to tune it.
34
- page.setDefaultTimeout(90_000);
35
- page.setDefaultNavigationTimeout(90_000);
31
+ // Cap individual browser operations, then separately watch for per-file
32
+ // progress so large suites can run longer than this without hiding hangs.
33
+ page.setDefaultTimeout(BROWSER_TEST_FILE_TIMEOUT_MS);
34
+ page.setDefaultNavigationTimeout(BROWSER_TEST_FILE_TIMEOUT_MS);
36
35
  if (options.console) {
37
36
  page.on('console', (msg) => console.log(`${colors.dim('[browser console]')} ${msg.text()}`));
38
37
  }
@@ -47,6 +46,26 @@ export async function runBrowserTests(options) {
47
46
  let totalSkipped = 0;
48
47
  let totalTodo = 0;
49
48
  let rootDir = getBrowserTestRootDir();
49
+ let completedFiles = 0;
50
+ let totalFiles = options.testFiles?.length ?? 0;
51
+ let progressTimeoutId;
52
+ let rejectProgressTimeout = () => { };
53
+ let progressTimeoutPromise = new Promise((_, reject) => {
54
+ rejectProgressTimeout = reject;
55
+ });
56
+ function clearProgressTimeout() {
57
+ if (progressTimeoutId !== undefined) {
58
+ clearTimeout(progressTimeoutId);
59
+ progressTimeoutId = undefined;
60
+ }
61
+ }
62
+ function resetProgressTimeout() {
63
+ clearProgressTimeout();
64
+ progressTimeoutId = setTimeout(() => {
65
+ let progress = totalFiles > 0 ? ` (${completedFiles}/${totalFiles} files completed)` : '';
66
+ rejectProgressTimeout(new Error(`Timed out waiting ${BROWSER_TEST_FILE_TIMEOUT_MS}ms for browser test progress${progress}`));
67
+ }, BROWSER_TEST_FILE_TIMEOUT_MS);
68
+ }
50
69
  await page.route('**/file-results', async (route) => {
51
70
  let results = route.request().postDataJSON();
52
71
  for (let test of results.tests) {
@@ -58,6 +77,8 @@ export async function runBrowserTests(options) {
58
77
  totalFailed += results.failed;
59
78
  totalSkipped += results.skipped;
60
79
  totalTodo += results.todo;
80
+ completedFiles++;
81
+ resetProgressTimeout();
61
82
  await route.fulfill({ status: 200 });
62
83
  });
63
84
  // Fail the tests if any /scripts/ request fails (harness scripts, test
@@ -77,8 +98,19 @@ export async function runBrowserTests(options) {
77
98
  });
78
99
  // Prevent unhandled rejection if we fail before setting up the listener
79
100
  errorPromise.catch(() => { });
80
- await page.goto(options.baseUrl);
81
- await Promise.race([page.waitForFunction('window.__testsDone'), errorPromise]);
101
+ progressTimeoutPromise.catch(() => { });
102
+ resetProgressTimeout();
103
+ try {
104
+ await page.goto(options.baseUrl);
105
+ await Promise.race([
106
+ page.waitForFunction('window.__testsDone', undefined, { timeout: 0 }),
107
+ errorPromise,
108
+ progressTimeoutPromise,
109
+ ]);
110
+ }
111
+ finally {
112
+ clearProgressTimeout();
113
+ }
82
114
  if (coverageEnabled) {
83
115
  let entries = (await page.coverage.stopJSCoverage());
84
116
  if (entries.length > 0) {