@rstest/browser 0.10.2 → 0.10.4

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 (43) hide show
  1. package/dist/{323.js → 626.js} +2 -3
  2. package/dist/browser-container/container-static/js/{188.480c3c49b5.js → 3.d3d6c6e4cf.js} +2908 -2915
  3. package/dist/browser-container/container-static/js/3.d3d6c6e4cf.js.LICENSE.txt +1 -0
  4. package/dist/browser-container/container-static/js/{index.29fc60be33.js → index.8fb1588e4a.js} +431 -455
  5. package/dist/browser-container/container-static/js/{lib-react.5ae9b90ee5.js → lib-react.62b27a21db.js} +21 -21
  6. package/dist/browser-container/container-static/js/lib-react.62b27a21db.js.LICENSE.txt +1 -0
  7. package/dist/browser-container/index.html +1 -1
  8. package/dist/browser.js +5 -5
  9. package/dist/client/dispatchTransport.d.ts +19 -0
  10. package/dist/client/public.d.ts +3 -3
  11. package/dist/client/sourceMapSupport.d.ts +1 -1
  12. package/dist/concurrency.d.ts +3 -3
  13. package/dist/configValidation.d.ts +2 -2
  14. package/dist/dispatchCapabilities.d.ts +1 -1
  15. package/dist/hostController.d.ts +3 -3
  16. package/dist/index.d.ts +5 -5
  17. package/dist/index.js +209 -221
  18. package/dist/protocol.d.ts +45 -18
  19. package/dist/providers/index.d.ts +11 -1
  20. package/dist/rpcProtocol.d.ts +32 -18
  21. package/dist/rslib-runtime.js +9 -5
  22. package/dist/viewportPresets.d.ts +5 -0
  23. package/package.json +8 -8
  24. package/src/AGENTS.md +7 -1
  25. package/src/client/dispatchTransport.ts +63 -1
  26. package/src/client/entry.ts +25 -58
  27. package/src/client/public.ts +3 -3
  28. package/src/client/snapshot.ts +24 -22
  29. package/src/client/sourceMapSupport.ts +1 -1
  30. package/src/concurrency.ts +6 -20
  31. package/src/configValidation.ts +2 -2
  32. package/src/dispatchCapabilities.ts +102 -83
  33. package/src/env.d.ts +6 -2
  34. package/src/hostController.ts +76 -42
  35. package/src/index.ts +22 -11
  36. package/src/protocol.ts +66 -22
  37. package/src/providers/index.ts +11 -1
  38. package/src/providers/playwright/runtime.ts +104 -4
  39. package/src/rpcProtocol.ts +34 -21
  40. package/src/viewportPresets.ts +5 -0
  41. package/src/watchCliShortcuts.ts +2 -4
  42. package/dist/browser-container/container-static/js/188.480c3c49b5.js.LICENSE.txt +0 -1
  43. package/dist/browser-container/container-static/js/lib-react.5ae9b90ee5.js.LICENSE.txt +0 -1
@@ -1,5 +1,9 @@
1
- import os from 'node:os';
2
- import type { Rstest } from '@rstest/core/browser';
1
+ import { getNumCpus, parseWorkers } from '@rstest/core/internal/browser';
2
+ import type { Rstest } from '@rstest/core/internal/browser';
3
+
4
+ // Re-export the shared worker primitives so existing consumers (and unit tests
5
+ // importing from `../src/concurrency`) keep a stable import surface.
6
+ export { getNumCpus, parseWorkers };
3
7
 
4
8
  // Shared headless concurrency policy.
5
9
  // Keep this in one place so executors reuse the same worker semantics.
@@ -13,24 +17,6 @@ type HeadlessConcurrencyContext = Pick<Rstest, 'command'> & {
13
17
  };
14
18
  };
15
19
 
16
- const getNumCpus = (): number => {
17
- return os.availableParallelism?.() ?? os.cpus().length;
18
- };
19
-
20
- export const parseWorkers = (
21
- maxWorkers: string | number,
22
- numCpus = getNumCpus(),
23
- ): number => {
24
- const parsed = Number.parseInt(maxWorkers.toString(), 10);
25
-
26
- if (typeof maxWorkers === 'string' && maxWorkers.trim().endsWith('%')) {
27
- const workers = Math.floor((parsed / 100) * numCpus);
28
- return Math.max(workers, 1);
29
- }
30
-
31
- return parsed > 0 ? parsed : 1;
32
- };
33
-
34
20
  export const resolveDefaultHeadlessWorkers = (
35
21
  command: HeadlessConcurrencyContext['command'],
36
22
  numCpus = getNumCpus(),
@@ -1,4 +1,4 @@
1
- import type { Rstest } from '@rstest/core/browser';
1
+ import type { RstestContext } from '@rstest/core/internal/browser';
2
2
  import { resolveBrowserViewportPreset } from './viewportPresets';
3
3
 
4
4
  const SUPPORTED_PROVIDERS = ['playwright'] as const;
@@ -42,7 +42,7 @@ const validateViewport = (viewport: unknown): void => {
42
42
  );
43
43
  };
44
44
 
45
- export const validateBrowserConfig = (context: Rstest): void => {
45
+ export const validateBrowserConfig = (context: RstestContext): void => {
46
46
  for (const project of context.projects) {
47
47
  const { browser, output } = project.normalizedConfig;
48
48
  if (!browser.enabled) {
@@ -1,9 +1,16 @@
1
- import type { Reporter } from '@rstest/core/browser';
1
+ import type { Reporter } from '@rstest/core/internal/browser';
2
2
  import { HostDispatchRouter } from './dispatchRouter';
3
+ import {
4
+ DISPATCH_NAMESPACE_RUNNER,
5
+ DISPATCH_NAMESPACE_SNAPSHOT,
6
+ } from './protocol';
3
7
  import type {
4
8
  BrowserClientMessage,
5
9
  BrowserDispatchHandler,
6
10
  BrowserDispatchRequest,
11
+ RunnerDispatchMethod,
12
+ SnapshotRpcMethod,
13
+ SnapshotRpcMethodArgs,
7
14
  SnapshotRpcRequest,
8
15
  } from './protocol';
9
16
 
@@ -52,37 +59,50 @@ type CreateHostDispatchRouterOptions = {
52
59
  onDuplicateNamespace?: (namespace: string) => void;
53
60
  };
54
61
 
62
+ /**
63
+ * Builds a typed {@link SnapshotRpcRequest} from the untrusted wire envelope,
64
+ * one builder per method. Keyed by {@link SnapshotRpcMethod}, so adding or
65
+ * renaming a method in the union forces a matching entry here — a stale name
66
+ * becomes a compile error instead of silently falling through to `null`. The
67
+ * `args` casts are the unavoidable trust boundary for inbound wire data.
68
+ */
69
+ const snapshotRequestBuilders: {
70
+ [M in SnapshotRpcMethod]: (
71
+ id: string,
72
+ args: BrowserDispatchRequest['args'],
73
+ ) => Extract<SnapshotRpcRequest, { method: M }>;
74
+ } = {
75
+ resolveSnapshotPath: (id, args) => ({
76
+ id,
77
+ method: 'resolveSnapshotPath',
78
+ args: args as SnapshotRpcMethodArgs['resolveSnapshotPath'],
79
+ }),
80
+ readSnapshotFile: (id, args) => ({
81
+ id,
82
+ method: 'readSnapshotFile',
83
+ args: args as SnapshotRpcMethodArgs['readSnapshotFile'],
84
+ }),
85
+ saveSnapshotFile: (id, args) => ({
86
+ id,
87
+ method: 'saveSnapshotFile',
88
+ args: args as SnapshotRpcMethodArgs['saveSnapshotFile'],
89
+ }),
90
+ removeSnapshotFile: (id, args) => ({
91
+ id,
92
+ method: 'removeSnapshotFile',
93
+ args: args as SnapshotRpcMethodArgs['removeSnapshotFile'],
94
+ }),
95
+ };
96
+
55
97
  const toSnapshotRpcRequest = (
56
98
  request: BrowserDispatchRequest,
57
99
  ): SnapshotRpcRequest | null => {
58
- switch (request.method) {
59
- case 'resolveSnapshotPath':
60
- return {
61
- id: request.requestId,
62
- method: 'resolveSnapshotPath',
63
- args: request.args as { testPath: string },
64
- };
65
- case 'readSnapshotFile':
66
- return {
67
- id: request.requestId,
68
- method: 'readSnapshotFile',
69
- args: request.args as { filepath: string },
70
- };
71
- case 'saveSnapshotFile':
72
- return {
73
- id: request.requestId,
74
- method: 'saveSnapshotFile',
75
- args: request.args as { filepath: string; content: string },
76
- };
77
- case 'removeSnapshotFile':
78
- return {
79
- id: request.requestId,
80
- method: 'removeSnapshotFile',
81
- args: request.args as { filepath: string },
82
- };
83
- default:
84
- return null;
85
- }
100
+ const builder = snapshotRequestBuilders[
101
+ request.method as SnapshotRpcMethod
102
+ ] as
103
+ | ((id: string, args: BrowserDispatchRequest['args']) => SnapshotRpcRequest)
104
+ | undefined;
105
+ return builder ? builder(request.requestId, request.args) : null;
86
106
  };
87
107
 
88
108
  export const createHostDispatchRouter = ({
@@ -94,61 +114,60 @@ export const createHostDispatchRouter = ({
94
114
  }: CreateHostDispatchRouterOptions): HostDispatchRouter => {
95
115
  const router = new HostDispatchRouter(routerOptions);
96
116
 
97
- router.register('runner', async (request: BrowserDispatchRequest) => {
98
- switch (request.method) {
99
- case 'file-start':
100
- await runnerCallbacks.onTestFileStart(
101
- request.args as RunnerPayload<'file-start'>,
102
- );
103
- break;
104
- case 'file-ready':
105
- await runnerCallbacks.onTestFileReady(
106
- request.args as RunnerDispatchFileReadyPayload,
107
- );
108
- break;
109
- case 'suite-start':
110
- await runnerCallbacks.onTestSuiteStart(
111
- request.args as RunnerDispatchSuiteStartPayload,
112
- );
113
- break;
114
- case 'suite-result':
115
- await runnerCallbacks.onTestSuiteResult(
116
- request.args as RunnerDispatchSuiteResultPayload,
117
- );
118
- break;
119
- case 'case-start':
120
- await runnerCallbacks.onTestCaseStart(
121
- request.args as RunnerDispatchCaseStartPayload,
122
- );
123
- break;
124
- case 'case-result':
125
- await runnerCallbacks.onTestCaseResult(
126
- request.args as RunnerPayload<'case-result'>,
127
- );
128
- break;
129
- case 'file-complete':
130
- await runnerCallbacks.onTestFileComplete(
131
- request.args as RunnerPayload<'file-complete'>,
132
- );
133
- break;
134
- case 'log':
135
- await runnerCallbacks.onLog(request.args as RunnerPayload<'log'>);
136
- break;
137
- case 'fatal':
138
- await runnerCallbacks.onFatal(request.args as RunnerPayload<'fatal'>);
139
- break;
140
- default:
141
- break;
142
- }
143
- });
117
+ // Keyed by RunnerDispatchMethod so adding a runner method to that union forces
118
+ // a handler entry here (a missing key is a compile error) — the previous
119
+ // `switch` with `default: break` silently dropped unhandled methods. The
120
+ // `args` casts are checked against each callback's parameter type, so a wrong
121
+ // payload type also fails to compile.
122
+ const runnerMethodHandlers: {
123
+ [M in RunnerDispatchMethod]: (
124
+ args: BrowserDispatchRequest['args'],
125
+ ) => Promise<void>;
126
+ } = {
127
+ 'file-start': (args) =>
128
+ runnerCallbacks.onTestFileStart(args as RunnerPayload<'file-start'>),
129
+ 'file-ready': (args) =>
130
+ runnerCallbacks.onTestFileReady(args as RunnerDispatchFileReadyPayload),
131
+ 'suite-start': (args) =>
132
+ runnerCallbacks.onTestSuiteStart(args as RunnerDispatchSuiteStartPayload),
133
+ 'suite-result': (args) =>
134
+ runnerCallbacks.onTestSuiteResult(
135
+ args as RunnerDispatchSuiteResultPayload,
136
+ ),
137
+ 'case-start': (args) =>
138
+ runnerCallbacks.onTestCaseStart(args as RunnerDispatchCaseStartPayload),
139
+ 'case-result': (args) =>
140
+ runnerCallbacks.onTestCaseResult(args as RunnerPayload<'case-result'>),
141
+ 'file-complete': (args) =>
142
+ runnerCallbacks.onTestFileComplete(
143
+ args as RunnerPayload<'file-complete'>,
144
+ ),
145
+ log: (args) => runnerCallbacks.onLog(args as RunnerPayload<'log'>),
146
+ fatal: (args) => runnerCallbacks.onFatal(args as RunnerPayload<'fatal'>),
147
+ };
144
148
 
145
- router.register('snapshot', async (request: BrowserDispatchRequest) => {
146
- const snapshotRequest = toSnapshotRpcRequest(request);
147
- if (!snapshotRequest) {
148
- return undefined;
149
- }
150
- return runSnapshotRpc(snapshotRequest);
151
- });
149
+ router.register(
150
+ DISPATCH_NAMESPACE_RUNNER,
151
+ async (request: BrowserDispatchRequest) => {
152
+ // `request.method` is untrusted wire data, so the lookup may miss. Unknown
153
+ // methods are ignored for forward-compatibility with newer runners,
154
+ // matching the previous `default: break`.
155
+ await runnerMethodHandlers[request.method as RunnerDispatchMethod]?.(
156
+ request.args,
157
+ );
158
+ },
159
+ );
160
+
161
+ router.register(
162
+ DISPATCH_NAMESPACE_SNAPSHOT,
163
+ async (request: BrowserDispatchRequest) => {
164
+ const snapshotRequest = toSnapshotRpcRequest(request);
165
+ if (!snapshotRequest) {
166
+ return undefined;
167
+ }
168
+ return runSnapshotRpc(snapshotRequest);
169
+ },
170
+ );
152
171
 
153
172
  for (const [namespace, handler] of extensionHandlers ?? []) {
154
173
  if (router.has(namespace)) {
package/src/env.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DISPATCH_MESSAGE_TYPE, DISPATCH_RPC_BRIDGE_NAME } from './protocol';
1
2
  import type {
2
3
  BrowserClientMessage,
3
4
  BrowserDispatchRequest,
@@ -38,8 +39,11 @@ declare global {
38
39
 
39
40
  interface Window {
40
41
  __RSTEST_BROWSER_OPTIONS__?: BrowserHostConfig;
41
- __rstest_dispatch__?: (message: BrowserClientMessage) => void;
42
- __rstest_dispatch_rpc__?: (
42
+ // Keyed by the sentinel constants so the declared global name and the
43
+ // constant are the same source — renaming a constant moves this key with it,
44
+ // and every `window[CONST]` access site stays in lockstep automatically.
45
+ [DISPATCH_MESSAGE_TYPE]?: (message: BrowserClientMessage) => void;
46
+ [DISPATCH_RPC_BRIDGE_NAME]?: (
43
47
  request: BrowserDispatchRequest,
44
48
  ) => Promise<unknown>;
45
49
  __rstest_container_dispatch__?: (data: unknown) => void;
@@ -11,6 +11,7 @@ import {
11
11
  type CoverageMapData,
12
12
  color,
13
13
  createCoverageProvider,
14
+ DEFAULT_TEST_TIMEOUT,
14
15
  type FormattedError,
15
16
  getNoTestFilesMessage,
16
17
  getSetupFiles,
@@ -22,16 +23,17 @@ import {
22
23
  PhaseTracker,
23
24
  type ProjectContext,
24
25
  type Reporter,
25
- type Rstest,
26
+ type RstestContext,
26
27
  type RuntimeConfig,
27
28
  resolveProjectBuildCache,
29
+ RSTEST_ENV_SYMBOL_KEY,
28
30
  rsbuild,
29
31
  serializableConfig,
30
32
  type Test,
31
33
  type TestFileResult,
32
34
  type TestResult,
33
35
  type UserConsoleLog,
34
- } from '@rstest/core/browser';
36
+ } from '@rstest/core/internal/browser';
35
37
  import { type BirpcReturn, createBirpc } from 'birpc';
36
38
  import openEditor from 'open-editor';
37
39
  import { basename, dirname, join, normalize, relative, resolve } from 'pathe';
@@ -52,6 +54,7 @@ import type {
52
54
  BrowserDispatchRequest,
53
55
  BrowserDispatchResponse,
54
56
  BrowserHostConfig,
57
+ BrowserLogPayload,
55
58
  BrowserProjectRuntime,
56
59
  BrowserRpcRequest,
57
60
  BrowserViewport,
@@ -60,6 +63,7 @@ import type {
60
63
  } from './protocol';
61
64
  import {
62
65
  DISPATCH_MESSAGE_TYPE,
66
+ DISPATCH_NAMESPACE_BROWSER,
63
67
  DISPATCH_NAMESPACE_RUNNER,
64
68
  validateBrowserRpcRequest,
65
69
  } from './protocol';
@@ -97,6 +101,15 @@ type RsbuildInstance = rsbuild.RsbuildInstance;
97
101
  const __dirname = dirname(fileURLToPath(import.meta.url));
98
102
  const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
99
103
 
104
+ /**
105
+ * Extra time added on top of a file's `testTimeout` before the host gives up
106
+ * waiting for that file's RPC to settle. Covers fixed per-file overhead (page
107
+ * navigation, runner boot) that is not part of the user's test budget. The
108
+ * headless and headed scheduling paths both apply this same buffer, so it lives
109
+ * here as one constant to keep their per-file timeout semantics identical.
110
+ */
111
+ const PER_FILE_TIMEOUT_BUFFER_MS = 30_000;
112
+
100
113
  /**
101
114
  * Monotonic counter for synthetic per-file Perfetto `pid` values in `--trace`
102
115
  * mode. Browser host runs every test file inside the same Node process, so
@@ -165,18 +178,8 @@ type TestFileStartPayload = {
165
178
  projectName: string;
166
179
  };
167
180
 
168
- /** Payload for log event */
169
- type LogPayload = {
170
- level: 'log' | 'warn' | 'error' | 'info' | 'debug';
171
- content: string;
172
- taskId?: string;
173
- taskName?: string;
174
- taskParentNames?: string[];
175
- taskType?: 'file' | 'suite' | 'case';
176
- testPath: string;
177
- type: 'stdout' | 'stderr';
178
- trace?: string;
179
- };
181
+ /** Payload for log event — single-sourced from the wire protocol. */
182
+ type LogPayload = BrowserLogPayload;
180
183
 
181
184
  /** Payload for fatal error event */
182
185
  type FatalPayload = {
@@ -830,11 +833,16 @@ const getRuntimeConfigFromProject = (
830
833
  } = project.normalizedConfig;
831
834
 
832
835
  return {
833
- // Propagate NODE_ENV from the host so `import.meta.env.NODE_ENV` resolves
834
- // to `'test'` in browser tests (matches Node mode). User-supplied `env`
835
- // wins so explicit overrides still take effect.
836
+ // Propagate NODE_ENV and the RSTEST flag from the host so
837
+ // `process.env.NODE_ENV` / `process.env.RSTEST` (rewritten to the
838
+ // `RSTEST_ENV_SYMBOL_KEY` symbol store) resolve in browser tests the same
839
+ // way they do in Node mode, where `prepare.ts` sets them on the real
840
+ // `process.env`.
841
+ // User-supplied `env` wins so explicit overrides still take effect.
842
+ // See https://github.com/web-infra-dev/rstest/issues/1351
836
843
  env: {
837
844
  NODE_ENV: process.env.NODE_ENV,
845
+ RSTEST: 'true',
838
846
  ...env,
839
847
  },
840
848
  testNamePattern,
@@ -864,7 +872,7 @@ const getRuntimeConfigFromProject = (
864
872
  };
865
873
  };
866
874
 
867
- const getBrowserProjects = (context: Rstest): ProjectContext[] => {
875
+ const getBrowserProjects = (context: RstestContext): ProjectContext[] => {
868
876
  return context.projects.filter(
869
877
  (project) => project.normalizedConfig.browser.enabled,
870
878
  );
@@ -936,7 +944,7 @@ const resolveProviderForTestPath = ({
936
944
  };
937
945
 
938
946
  const collectProjectEntries = async (
939
- context: Rstest,
947
+ context: RstestContext,
940
948
  ): Promise<BrowserProjectEntries[]> => {
941
949
  // Only collect entries for browser mode projects
942
950
  const browserProjects = getBrowserProjects(context);
@@ -1211,7 +1219,7 @@ const createBrowserRuntime = async ({
1211
1219
  containerDevServer,
1212
1220
  forceHeadless,
1213
1221
  }: {
1214
- context: Rstest;
1222
+ context: RstestContext;
1215
1223
  manifestPath: string;
1216
1224
  manifestSource: string;
1217
1225
  tempDir: string;
@@ -1257,7 +1265,7 @@ const createBrowserRuntime = async ({
1257
1265
 
1258
1266
  // Rstest internal aliases that must not be overridden by user config
1259
1267
  const browserRuntimePath = fileURLToPath(
1260
- import.meta.resolve('@rstest/core/browser-runtime'),
1268
+ import.meta.resolve('@rstest/core/internal/browser-runtime'),
1261
1269
  );
1262
1270
 
1263
1271
  const rstestInternalAliases = {
@@ -1268,7 +1276,7 @@ const createBrowserRuntime = async ({
1268
1276
  '@rstest/browser': resolveBrowserFile('browser.ts'),
1269
1277
  // Browser runtime APIs for entry.ts and public.ts
1270
1278
  // Uses dist file with extractSourceMap to preserve sourcemap chain for inline snapshots
1271
- '@rstest/core/browser-runtime': browserRuntimePath,
1279
+ '@rstest/core/internal/browser-runtime': browserRuntimePath,
1272
1280
  '@sinonjs/fake-timers': resolveBrowserFile('client/fakeTimersStub.ts'),
1273
1281
  };
1274
1282
 
@@ -1329,6 +1337,13 @@ const createBrowserRuntime = async ({
1329
1337
  project.rootPath,
1330
1338
  ),
1331
1339
  );
1340
+ // rspack `define` replaces `process.env` / `import.meta.env` with
1341
+ // this literal expression. JSON.stringify reproduces the exact
1342
+ // double-quoted `"rstest.env"` text, so the owned key can never
1343
+ // drift from the runtime `Symbol.for(RSTEST_ENV_SYMBOL_KEY)` sites.
1344
+ const rstestEnvDefine = `globalThis[Symbol.for(${JSON.stringify(
1345
+ RSTEST_ENV_SYMBOL_KEY,
1346
+ )})]`;
1332
1347
  // Merge order: current config -> userConfig -> rstest required config (highest priority)
1333
1348
  const merged = mergeEnvironmentConfig(
1334
1349
  config,
@@ -1347,8 +1362,8 @@ const createBrowserRuntime = async ({
1347
1362
  },
1348
1363
  source: {
1349
1364
  define: {
1350
- 'process.env': 'globalThis[Symbol.for("rstest.env")]',
1351
- 'import.meta.env': 'globalThis[Symbol.for("rstest.env")]',
1365
+ 'process.env': rstestEnvDefine,
1366
+ 'import.meta.env': rstestEnvDefine,
1352
1367
  },
1353
1368
  },
1354
1369
  output: {
@@ -1677,7 +1692,7 @@ const createBrowserRuntime = async ({
1677
1692
  };
1678
1693
 
1679
1694
  async function resolveProjectEntries(
1680
- context: Rstest,
1695
+ context: RstestContext,
1681
1696
  shardedEntries?: Map<string, { entries: Record<string, string> }>,
1682
1697
  ): Promise<BrowserProjectEntries[]> {
1683
1698
  if (shardedEntries) {
@@ -1707,7 +1722,7 @@ async function resolveProjectEntries(
1707
1722
  // ============================================================================
1708
1723
 
1709
1724
  export const runBrowserController = async (
1710
- context: Rstest,
1725
+ context: RstestContext,
1711
1726
  options?: BrowserTestRunOptions,
1712
1727
  ): Promise<BrowserTestRunResult | void> => {
1713
1728
  const {
@@ -2073,7 +2088,9 @@ export const runBrowserController = async (
2073
2088
 
2074
2089
  // Get max testTimeout from all browser projects for RPC timeout
2075
2090
  const maxTestTimeoutForRpc = Math.max(
2076
- ...browserProjects.map((p) => p.normalizedConfig.testTimeout ?? 5000),
2091
+ ...browserProjects.map(
2092
+ (p) => p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT,
2093
+ ),
2077
2094
  );
2078
2095
 
2079
2096
  const hostOptions: BrowserHostConfig = {
@@ -2161,13 +2178,16 @@ export const runBrowserController = async (
2161
2178
  }
2162
2179
  };
2163
2180
 
2164
- runtime.dispatchHandlers.set('browser', async (dispatchRequest) => {
2165
- const request = validateBrowserRpcRequest(dispatchRequest.args);
2166
- return dispatchBrowserRpcRequest({
2167
- request,
2168
- target: dispatchRequest.target,
2169
- });
2170
- });
2181
+ runtime.dispatchHandlers.set(
2182
+ DISPATCH_NAMESPACE_BROWSER,
2183
+ async (dispatchRequest) => {
2184
+ const request = validateBrowserRpcRequest(dispatchRequest.args);
2185
+ return dispatchBrowserRpcRequest({
2186
+ request,
2187
+ target: dispatchRequest.target,
2188
+ });
2189
+ },
2190
+ );
2171
2191
 
2172
2192
  runtime.setContainerOptions(hostOptions);
2173
2193
 
@@ -2397,7 +2417,9 @@ export const runBrowserController = async (
2397
2417
  };
2398
2418
 
2399
2419
  const shouldEmitUserConsoleLog = (log: UserConsoleLog): boolean => {
2400
- return context.normalizedConfig.onConsoleLog?.(log.content) !== false;
2420
+ return (
2421
+ context.normalizedConfig.onConsoleLog?.(log.content, log.type) !== false
2422
+ );
2401
2423
  };
2402
2424
 
2403
2425
  const emitUserConsoleLog = async (log: UserConsoleLog): Promise<void> => {
@@ -2505,8 +2527,12 @@ export const runBrowserController = async (
2505
2527
  );
2506
2528
  case 'removeSnapshotFile':
2507
2529
  return snapshotRpcMethods.removeSnapshotFile(request.args.filepath);
2508
- default:
2509
- return undefined;
2530
+ default: {
2531
+ // Exhaustiveness guard: a new SnapshotRpcRequest method without a case
2532
+ // here fails to compile rather than silently returning undefined.
2533
+ const _exhaustive: never = request;
2534
+ return _exhaustive;
2535
+ }
2510
2536
  }
2511
2537
  };
2512
2538
 
@@ -2596,7 +2622,7 @@ export const runBrowserController = async (
2596
2622
  message: BrowserClientMessage,
2597
2623
  ): Promise<void> => {
2598
2624
  const response = await dispatchRouter.dispatch({
2599
- requestId: nextDispatchRequestId('runner'),
2625
+ requestId: nextDispatchRequestId(DISPATCH_NAMESPACE_RUNNER),
2600
2626
  runToken: run.token,
2601
2627
  namespace: DISPATCH_NAMESPACE_RUNNER,
2602
2628
  method: message.type,
@@ -2653,7 +2679,7 @@ export const runBrowserController = async (
2653
2679
  );
2654
2680
  const perFileTimeoutMs =
2655
2681
  (projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) +
2656
- 30_000;
2682
+ PER_FILE_TIMEOUT_BUFFER_MS;
2657
2683
 
2658
2684
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
2659
2685
 
@@ -3052,6 +3078,12 @@ export const runBrowserController = async (
3052
3078
  }
3053
3079
 
3054
3080
  let currentTestFiles = allTestFiles;
3081
+ // Coincidentally equal to the runner-side CONFIG_WAIT_TIMEOUT_MS and
3082
+ // DEFAULT_RPC_TIMEOUT_MS (client/entry.ts, client/dispatchTransport.ts) but
3083
+ // semantically distinct and in a different runtime, so deliberately NOT shared
3084
+ // with them. Invariant worth preserving: a runner must be able to receive its
3085
+ // config (config-wait) before the host declares its frames un-ready, i.e.
3086
+ // CONFIG_WAIT_TIMEOUT_MS <= RUNNER_FRAMES_READY_TIMEOUT_MS.
3055
3087
  const RUNNER_FRAMES_READY_TIMEOUT_MS = 30_000;
3056
3088
  let currentRunnerFramesSignature: string | null = null;
3057
3089
  const runnerFramesWaiters = new Map<string, Set<() => void>>();
@@ -3139,7 +3171,7 @@ export const runBrowserController = async (
3139
3171
  );
3140
3172
  return (
3141
3173
  (projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) +
3142
- 30_000
3174
+ PER_FILE_TIMEOUT_BUFFER_MS
3143
3175
  );
3144
3176
  };
3145
3177
 
@@ -3602,7 +3634,7 @@ export type ListBrowserTestsResult = {
3602
3634
  * and collects their test structure (describe/test declarations).
3603
3635
  */
3604
3636
  export const listBrowserTests = async (
3605
- context: Rstest,
3637
+ context: RstestContext,
3606
3638
  options?: {
3607
3639
  shardedEntries?: Map<string, { entries: Record<string, string> }>;
3608
3640
  },
@@ -3681,7 +3713,9 @@ export const listBrowserTests = async (
3681
3713
 
3682
3714
  // Get max testTimeout from all browser projects for RPC timeout
3683
3715
  const maxTestTimeoutForRpc = Math.max(
3684
- ...browserProjects.map((p) => p.normalizedConfig.testTimeout ?? 5000),
3716
+ ...browserProjects.map(
3717
+ (p) => p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT,
3718
+ ),
3685
3719
  );
3686
3720
 
3687
3721
  const hostOptions: BrowserHostConfig = {
package/src/index.ts CHANGED
@@ -1,34 +1,45 @@
1
1
  import type {
2
+ BrowserHostModule,
2
3
  BrowserTestRunOptions,
3
4
  BrowserTestRunResult,
4
- Rstest,
5
- } from '@rstest/core/browser';
5
+ RstestContext,
6
+ } from '@rstest/core/internal/browser';
7
+ import { validateBrowserConfig } from './configValidation';
6
8
  import {
7
9
  type ListBrowserTestsResult,
8
10
  listBrowserTests as listBrowserTestsImpl,
9
11
  runBrowserController,
10
12
  } from './hostController';
11
13
 
12
- export { validateBrowserConfig } from './configValidation';
13
- export {
14
- BROWSER_VIEWPORT_PRESET_DIMENSIONS,
15
- BROWSER_VIEWPORT_PRESET_IDS,
16
- resolveBrowserViewportPreset,
17
- } from './viewportPresets';
14
+ export { validateBrowserConfig };
18
15
 
19
16
  export async function runBrowserTests(
20
- context: Rstest,
17
+ context: RstestContext,
21
18
  options?: BrowserTestRunOptions,
22
19
  ): Promise<BrowserTestRunResult | void> {
23
20
  return runBrowserController(context, options);
24
21
  }
25
22
 
26
23
  export async function listBrowserTests(
27
- context: Rstest,
24
+ context: RstestContext,
25
+ options?: Pick<BrowserTestRunOptions, 'shardedEntries'>,
28
26
  ): Promise<ListBrowserTestsResult> {
29
- return listBrowserTestsImpl(context);
27
+ // Forward `options` (e.g. `shardedEntries`) so `rstest list --shard` lists
28
+ // only the current shard's browser test files, matching the run path.
29
+ return listBrowserTestsImpl(context, options);
30
30
  }
31
31
 
32
+ /**
33
+ * Compile-time guard: ensure the public host exports satisfy the core-owned
34
+ * {@link BrowserHostModule} contract. This catches drift such as a dropped
35
+ * `options` argument at the load boundary. No runtime side effect.
36
+ */
37
+ void ({
38
+ validateBrowserConfig,
39
+ runBrowserTests,
40
+ listBrowserTests,
41
+ } satisfies BrowserHostModule);
42
+
32
43
  export type {
33
44
  BrowserTestRunOptions,
34
45
  BrowserTestRunResult,