@rstest/browser 0.10.3 → 0.10.5

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 (64) hide show
  1. package/dist/626.js +2 -1
  2. package/dist/browser-container/container-static/css/index.0521ff95b1.css +1 -0
  3. package/dist/browser-container/container-static/js/{27.099feaf1da.js → 854.c59f71483d.js} +5728 -5756
  4. package/dist/browser-container/container-static/js/854.c59f71483d.js.LICENSE.txt +1 -0
  5. package/dist/browser-container/container-static/js/{index.be8c8d2626.js → index.fedc31dff3.js} +109 -136
  6. package/dist/browser-container/container-static/js/{lib-react.5ae9b90ee5.js → lib-react.62b27a21db.js} +21 -21
  7. package/dist/browser-container/container-static/js/lib-react.62b27a21db.js.LICENSE.txt +1 -0
  8. package/dist/browser-container/index.html +1 -1
  9. package/dist/browser.js +4 -4
  10. package/dist/browserRpcRegistry.d.ts +1 -2
  11. package/dist/client/api.d.ts +1 -1
  12. package/dist/client/dispatchTransport.d.ts +18 -1
  13. package/dist/client/locator.d.ts +2 -3
  14. package/dist/concurrency.d.ts +5 -3
  15. package/dist/configValidation.d.ts +2 -2
  16. package/dist/dispatchCapabilities.d.ts +3 -3
  17. package/dist/dispatchRouter.d.ts +2 -3
  18. package/dist/headedSerialTaskQueue.d.ts +5 -5
  19. package/dist/headlessLatestRerunScheduler.d.ts +2 -3
  20. package/dist/headlessTransport.d.ts +2 -3
  21. package/dist/hostController.d.ts +12 -9
  22. package/dist/index.d.ts +6 -6
  23. package/dist/index.js +169 -216
  24. package/dist/protocol.d.ts +78 -55
  25. package/dist/providers/index.d.ts +18 -20
  26. package/dist/providers/playwright/dispatchBrowserRpc.d.ts +5 -3
  27. package/dist/providers/playwright/expectUtils.d.ts +2 -3
  28. package/dist/providers/playwright/implementation.d.ts +1 -1
  29. package/dist/providers/playwright/runtime.d.ts +1 -1
  30. package/dist/rpcProtocol.d.ts +36 -29
  31. package/dist/runSession.d.ts +3 -5
  32. package/dist/sessionRegistry.d.ts +2 -3
  33. package/dist/sourceMap/sourceMapLoader.d.ts +2 -2
  34. package/dist/viewportPresets.d.ts +7 -3
  35. package/dist/watchCliShortcuts.d.ts +1 -1
  36. package/dist/watchRerunPlanner.d.ts +2 -2
  37. package/package.json +7 -9
  38. package/src/AGENTS.md +7 -1
  39. package/src/browserRpcRegistry.ts +2 -2
  40. package/src/client/dispatchTransport.ts +63 -1
  41. package/src/client/entry.ts +25 -58
  42. package/src/client/public.ts +3 -3
  43. package/src/client/snapshot.ts +24 -22
  44. package/src/client/sourceMapSupport.ts +1 -1
  45. package/src/concurrency.ts +7 -21
  46. package/src/configValidation.ts +2 -2
  47. package/src/dispatchCapabilities.ts +102 -83
  48. package/src/env.d.ts +6 -2
  49. package/src/headedSerialTaskQueue.ts +5 -1
  50. package/src/hostController.ts +85 -43
  51. package/src/index.ts +22 -11
  52. package/src/protocol.ts +66 -22
  53. package/src/providers/index.ts +8 -1
  54. package/src/rpcProtocol.ts +34 -21
  55. package/src/viewportPresets.ts +5 -0
  56. package/src/watchCliShortcuts.ts +2 -4
  57. package/dist/browser-container/container-static/css/index.5c72297783.css +0 -1
  58. package/dist/browser-container/container-static/js/27.099feaf1da.js.LICENSE.txt +0 -1
  59. package/dist/browser-container/container-static/js/lib-react.5ae9b90ee5.js.LICENSE.txt +0 -1
  60. package/dist/client/entry.d.ts +0 -4
  61. package/dist/client/fakeTimersStub.d.ts +0 -25
  62. package/dist/client/public.d.ts +0 -9
  63. package/dist/client/snapshot.d.ts +0 -29
  64. package/dist/client/sourceMapSupport.d.ts +0 -42
@@ -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 = {
@@ -597,7 +600,15 @@ export const createBrowserLazyCompilationConfig = (
597
600
  };
598
601
  };
599
602
 
600
- export const createBrowserRsbuildDevConfig = (_isWatchMode: boolean) => {
603
+ export const createBrowserRsbuildDevConfig = (
604
+ _isWatchMode: boolean,
605
+ ): {
606
+ writeToDisk: boolean;
607
+ hmr: boolean;
608
+ client: {
609
+ logLevel: 'error';
610
+ };
611
+ } => {
601
612
  return {
602
613
  writeToDisk: isDebug(),
603
614
  // Keep HMR enabled in browser mode even for one-shot runs.
@@ -830,11 +841,16 @@ const getRuntimeConfigFromProject = (
830
841
  } = project.normalizedConfig;
831
842
 
832
843
  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.
844
+ // Propagate NODE_ENV and the RSTEST flag from the host so
845
+ // `process.env.NODE_ENV` / `process.env.RSTEST` (rewritten to the
846
+ // `RSTEST_ENV_SYMBOL_KEY` symbol store) resolve in browser tests the same
847
+ // way they do in Node mode, where `prepare.ts` sets them on the real
848
+ // `process.env`.
849
+ // User-supplied `env` wins so explicit overrides still take effect.
850
+ // See https://github.com/web-infra-dev/rstest/issues/1351
836
851
  env: {
837
852
  NODE_ENV: process.env.NODE_ENV,
853
+ RSTEST: 'true',
838
854
  ...env,
839
855
  },
840
856
  testNamePattern,
@@ -864,7 +880,7 @@ const getRuntimeConfigFromProject = (
864
880
  };
865
881
  };
866
882
 
867
- const getBrowserProjects = (context: Rstest): ProjectContext[] => {
883
+ const getBrowserProjects = (context: RstestContext): ProjectContext[] => {
868
884
  return context.projects.filter(
869
885
  (project) => project.normalizedConfig.browser.enabled,
870
886
  );
@@ -936,7 +952,7 @@ const resolveProviderForTestPath = ({
936
952
  };
937
953
 
938
954
  const collectProjectEntries = async (
939
- context: Rstest,
955
+ context: RstestContext,
940
956
  ): Promise<BrowserProjectEntries[]> => {
941
957
  // Only collect entries for browser mode projects
942
958
  const browserProjects = getBrowserProjects(context);
@@ -1211,7 +1227,7 @@ const createBrowserRuntime = async ({
1211
1227
  containerDevServer,
1212
1228
  forceHeadless,
1213
1229
  }: {
1214
- context: Rstest;
1230
+ context: RstestContext;
1215
1231
  manifestPath: string;
1216
1232
  manifestSource: string;
1217
1233
  tempDir: string;
@@ -1257,7 +1273,7 @@ const createBrowserRuntime = async ({
1257
1273
 
1258
1274
  // Rstest internal aliases that must not be overridden by user config
1259
1275
  const browserRuntimePath = fileURLToPath(
1260
- import.meta.resolve('@rstest/core/browser-runtime'),
1276
+ import.meta.resolve('@rstest/core/internal/browser-runtime'),
1261
1277
  );
1262
1278
 
1263
1279
  const rstestInternalAliases = {
@@ -1268,7 +1284,7 @@ const createBrowserRuntime = async ({
1268
1284
  '@rstest/browser': resolveBrowserFile('browser.ts'),
1269
1285
  // Browser runtime APIs for entry.ts and public.ts
1270
1286
  // Uses dist file with extractSourceMap to preserve sourcemap chain for inline snapshots
1271
- '@rstest/core/browser-runtime': browserRuntimePath,
1287
+ '@rstest/core/internal/browser-runtime': browserRuntimePath,
1272
1288
  '@sinonjs/fake-timers': resolveBrowserFile('client/fakeTimersStub.ts'),
1273
1289
  };
1274
1290
 
@@ -1329,6 +1345,13 @@ const createBrowserRuntime = async ({
1329
1345
  project.rootPath,
1330
1346
  ),
1331
1347
  );
1348
+ // rspack `define` replaces `process.env` / `import.meta.env` with
1349
+ // this literal expression. JSON.stringify reproduces the exact
1350
+ // double-quoted `"rstest.env"` text, so the owned key can never
1351
+ // drift from the runtime `Symbol.for(RSTEST_ENV_SYMBOL_KEY)` sites.
1352
+ const rstestEnvDefine = `globalThis[Symbol.for(${JSON.stringify(
1353
+ RSTEST_ENV_SYMBOL_KEY,
1354
+ )})]`;
1332
1355
  // Merge order: current config -> userConfig -> rstest required config (highest priority)
1333
1356
  const merged = mergeEnvironmentConfig(
1334
1357
  config,
@@ -1347,8 +1370,8 @@ const createBrowserRuntime = async ({
1347
1370
  },
1348
1371
  source: {
1349
1372
  define: {
1350
- 'process.env': 'globalThis[Symbol.for("rstest.env")]',
1351
- 'import.meta.env': 'globalThis[Symbol.for("rstest.env")]',
1373
+ 'process.env': rstestEnvDefine,
1374
+ 'import.meta.env': rstestEnvDefine,
1352
1375
  },
1353
1376
  },
1354
1377
  output: {
@@ -1677,7 +1700,7 @@ const createBrowserRuntime = async ({
1677
1700
  };
1678
1701
 
1679
1702
  async function resolveProjectEntries(
1680
- context: Rstest,
1703
+ context: RstestContext,
1681
1704
  shardedEntries?: Map<string, { entries: Record<string, string> }>,
1682
1705
  ): Promise<BrowserProjectEntries[]> {
1683
1706
  if (shardedEntries) {
@@ -1707,7 +1730,7 @@ async function resolveProjectEntries(
1707
1730
  // ============================================================================
1708
1731
 
1709
1732
  export const runBrowserController = async (
1710
- context: Rstest,
1733
+ context: RstestContext,
1711
1734
  options?: BrowserTestRunOptions,
1712
1735
  ): Promise<BrowserTestRunResult | void> => {
1713
1736
  const {
@@ -2073,7 +2096,9 @@ export const runBrowserController = async (
2073
2096
 
2074
2097
  // Get max testTimeout from all browser projects for RPC timeout
2075
2098
  const maxTestTimeoutForRpc = Math.max(
2076
- ...browserProjects.map((p) => p.normalizedConfig.testTimeout ?? 5000),
2099
+ ...browserProjects.map(
2100
+ (p) => p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT,
2101
+ ),
2077
2102
  );
2078
2103
 
2079
2104
  const hostOptions: BrowserHostConfig = {
@@ -2161,13 +2186,16 @@ export const runBrowserController = async (
2161
2186
  }
2162
2187
  };
2163
2188
 
2164
- runtime.dispatchHandlers.set('browser', async (dispatchRequest) => {
2165
- const request = validateBrowserRpcRequest(dispatchRequest.args);
2166
- return dispatchBrowserRpcRequest({
2167
- request,
2168
- target: dispatchRequest.target,
2169
- });
2170
- });
2189
+ runtime.dispatchHandlers.set(
2190
+ DISPATCH_NAMESPACE_BROWSER,
2191
+ async (dispatchRequest) => {
2192
+ const request = validateBrowserRpcRequest(dispatchRequest.args);
2193
+ return dispatchBrowserRpcRequest({
2194
+ request,
2195
+ target: dispatchRequest.target,
2196
+ });
2197
+ },
2198
+ );
2171
2199
 
2172
2200
  runtime.setContainerOptions(hostOptions);
2173
2201
 
@@ -2397,7 +2425,9 @@ export const runBrowserController = async (
2397
2425
  };
2398
2426
 
2399
2427
  const shouldEmitUserConsoleLog = (log: UserConsoleLog): boolean => {
2400
- return context.normalizedConfig.onConsoleLog?.(log.content) !== false;
2428
+ return (
2429
+ context.normalizedConfig.onConsoleLog?.(log.content, log.type) !== false
2430
+ );
2401
2431
  };
2402
2432
 
2403
2433
  const emitUserConsoleLog = async (log: UserConsoleLog): Promise<void> => {
@@ -2505,8 +2535,12 @@ export const runBrowserController = async (
2505
2535
  );
2506
2536
  case 'removeSnapshotFile':
2507
2537
  return snapshotRpcMethods.removeSnapshotFile(request.args.filepath);
2508
- default:
2509
- return undefined;
2538
+ default: {
2539
+ // Exhaustiveness guard: a new SnapshotRpcRequest method without a case
2540
+ // here fails to compile rather than silently returning undefined.
2541
+ const _exhaustive: never = request;
2542
+ return _exhaustive;
2543
+ }
2510
2544
  }
2511
2545
  };
2512
2546
 
@@ -2596,7 +2630,7 @@ export const runBrowserController = async (
2596
2630
  message: BrowserClientMessage,
2597
2631
  ): Promise<void> => {
2598
2632
  const response = await dispatchRouter.dispatch({
2599
- requestId: nextDispatchRequestId('runner'),
2633
+ requestId: nextDispatchRequestId(DISPATCH_NAMESPACE_RUNNER),
2600
2634
  runToken: run.token,
2601
2635
  namespace: DISPATCH_NAMESPACE_RUNNER,
2602
2636
  method: message.type,
@@ -2653,7 +2687,7 @@ export const runBrowserController = async (
2653
2687
  );
2654
2688
  const perFileTimeoutMs =
2655
2689
  (projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) +
2656
- 30_000;
2690
+ PER_FILE_TIMEOUT_BUFFER_MS;
2657
2691
 
2658
2692
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
2659
2693
 
@@ -3052,6 +3086,12 @@ export const runBrowserController = async (
3052
3086
  }
3053
3087
 
3054
3088
  let currentTestFiles = allTestFiles;
3089
+ // Coincidentally equal to the runner-side CONFIG_WAIT_TIMEOUT_MS and
3090
+ // DEFAULT_RPC_TIMEOUT_MS (client/entry.ts, client/dispatchTransport.ts) but
3091
+ // semantically distinct and in a different runtime, so deliberately NOT shared
3092
+ // with them. Invariant worth preserving: a runner must be able to receive its
3093
+ // config (config-wait) before the host declares its frames un-ready, i.e.
3094
+ // CONFIG_WAIT_TIMEOUT_MS <= RUNNER_FRAMES_READY_TIMEOUT_MS.
3055
3095
  const RUNNER_FRAMES_READY_TIMEOUT_MS = 30_000;
3056
3096
  let currentRunnerFramesSignature: string | null = null;
3057
3097
  const runnerFramesWaiters = new Map<string, Set<() => void>>();
@@ -3139,7 +3179,7 @@ export const runBrowserController = async (
3139
3179
  );
3140
3180
  return (
3141
3181
  (projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) +
3142
- 30_000
3182
+ PER_FILE_TIMEOUT_BUFFER_MS
3143
3183
  );
3144
3184
  };
3145
3185
 
@@ -3602,7 +3642,7 @@ export type ListBrowserTestsResult = {
3602
3642
  * and collects their test structure (describe/test declarations).
3603
3643
  */
3604
3644
  export const listBrowserTests = async (
3605
- context: Rstest,
3645
+ context: RstestContext,
3606
3646
  options?: {
3607
3647
  shardedEntries?: Map<string, { entries: Record<string, string> }>;
3608
3648
  },
@@ -3681,7 +3721,9 @@ export const listBrowserTests = async (
3681
3721
 
3682
3722
  // Get max testTimeout from all browser projects for RPC timeout
3683
3723
  const maxTestTimeoutForRpc = Math.max(
3684
- ...browserProjects.map((p) => p.normalizedConfig.testTimeout ?? 5000),
3724
+ ...browserProjects.map(
3725
+ (p) => p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT,
3726
+ ),
3685
3727
  );
3686
3728
 
3687
3729
  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,
package/src/protocol.ts CHANGED
@@ -1,15 +1,18 @@
1
- import type { DevicePreset } from '@rstest/core/browser';
1
+ import type { BrowserViewport } from '@rstest/core/internal/browser';
2
2
  import type {
3
3
  RuntimeConfig,
4
4
  TestFileResult,
5
5
  TestInfo,
6
6
  TestResult,
7
- } from '@rstest/core/browser-runtime';
7
+ } from '@rstest/core/internal/browser-runtime';
8
8
  import type { SnapshotUpdateState } from '@vitest/snapshot';
9
9
 
10
10
  export type {
11
11
  BrowserLocatorIR,
12
12
  BrowserRpcRequest,
13
+ SnapshotRpcCall,
14
+ SnapshotRpcMethod,
15
+ SnapshotRpcMethodArgs,
13
16
  SnapshotRpcRequest,
14
17
  } from './rpcProtocol';
15
18
  export { validateBrowserRpcRequest } from './rpcProtocol';
@@ -27,12 +30,10 @@ export const DISPATCH_METHOD_RPC = 'rpc';
27
30
 
28
31
  export type SerializedRuntimeConfig = RuntimeConfig;
29
32
 
30
- export type BrowserViewport =
31
- | {
32
- width: number;
33
- height: number;
34
- }
35
- | DevicePreset;
33
+ // `BrowserViewport` is a core config type (`@rstest/core` owns the canonical
34
+ // definition used by `NormalizedBrowserModeConfig`). Re-export it so the host
35
+ // assigns the SAME type across the seam instead of a hand-copied duplicate.
36
+ export type { BrowserViewport };
36
37
 
37
38
  export type BrowserProjectRuntime = {
38
39
  name: string;
@@ -58,6 +59,25 @@ export type TestFileInfo = {
58
59
  */
59
60
  export type BrowserExecutionMode = 'run' | 'collect';
60
61
 
62
+ /**
63
+ * Wire shape of a `log` client message payload. The host receives this and maps
64
+ * it onto core's {@link UserConsoleLog} (notably `level` → `name`); that mapper
65
+ * (`hostController.ts` `handleLog`) annotates its result as `UserConsoleLog`, so
66
+ * the map→core direction is compiler-checked. Owning the wire shape here as one
67
+ * named type keeps the host's input type from drifting away from the producer.
68
+ */
69
+ export type BrowserLogPayload = {
70
+ level: 'log' | 'warn' | 'error' | 'info' | 'debug';
71
+ content: string;
72
+ taskId?: string;
73
+ taskName?: string;
74
+ taskParentNames?: string[];
75
+ taskType?: 'file' | 'suite' | 'case';
76
+ testPath: string;
77
+ type: 'stdout' | 'stderr';
78
+ trace?: string;
79
+ };
80
+
61
81
  export type BrowserHostConfig = {
62
82
  rootPath: string;
63
83
  projects: BrowserProjectRuntime[];
@@ -101,20 +121,7 @@ export type BrowserClientMessage =
101
121
  }
102
122
  | { type: 'case-result'; payload: TestResult }
103
123
  | { type: 'file-complete'; payload: TestFileResult }
104
- | {
105
- type: 'log';
106
- payload: {
107
- level: 'log' | 'warn' | 'error' | 'info' | 'debug';
108
- content: string;
109
- taskId?: string;
110
- taskName?: string;
111
- taskParentNames?: string[];
112
- taskType?: 'file' | 'suite' | 'case';
113
- testPath: string;
114
- type: 'stdout' | 'stderr';
115
- trace?: string;
116
- };
117
- }
124
+ | { type: 'log'; payload: BrowserLogPayload }
118
125
  | {
119
126
  type: 'fatal';
120
127
  payload: { message: string; stack?: string };
@@ -134,6 +141,38 @@ export type BrowserClientMessage =
134
141
  payload: BrowserDispatchRequest;
135
142
  };
136
143
 
144
+ /**
145
+ * Lifecycle methods the runner emits via `dispatchRunnerLifecycle()` as
146
+ * dispatch-rpc-requests on the `runner` namespace (as opposed to the
147
+ * {@link BrowserClientMessage} types it `send()`s). The runner client imports
148
+ * this instead of redeclaring the list, so the emit site cannot drift from the
149
+ * host router.
150
+ */
151
+ export type RunnerLifecycleMethod =
152
+ | 'file-ready'
153
+ | 'suite-start'
154
+ | 'suite-result'
155
+ | 'case-start';
156
+
157
+ /**
158
+ * {@link BrowserClientMessage} types that are forwarded to the `runner`
159
+ * namespace (by message `type`) rather than handled at the transport layer.
160
+ * `Extract` keeps this a checked subset of the message union — renaming a
161
+ * message type drops it here, surfacing as a missing handler downstream.
162
+ */
163
+ type RunnerMessageMethod = Extract<
164
+ BrowserClientMessage['type'],
165
+ 'file-start' | 'case-result' | 'file-complete' | 'log' | 'fatal'
166
+ >;
167
+
168
+ /**
169
+ * Single source of truth for every method handled by the `runner` dispatch
170
+ * namespace. The host handler table is keyed by this union (a missing key is a
171
+ * compile error), so adding a runner method here forces a matching handler and
172
+ * cannot silently no-op at runtime.
173
+ */
174
+ export type RunnerDispatchMethod = RunnerLifecycleMethod | RunnerMessageMethod;
175
+
137
176
  /**
138
177
  * Transport-agnostic envelope used by host routing.
139
178
  * `namespace + method + args + target` describes an operation independent of
@@ -147,6 +186,11 @@ export type BrowserDispatchRequest = {
147
186
  namespace: string;
148
187
  method: string;
149
188
  args?: unknown;
189
+ // Routing reads `namespace`, `method`, `runToken`, and `target.sessionId`
190
+ // (see dispatchRouter.ts / dispatchBrowserRpcRequest). `target.testFile` and
191
+ // `target.projectName` are carried for diagnostics / forward-compatibility and
192
+ // are NOT consulted for routing today — adding a routing-relevant field here
193
+ // means wiring a reader on the host side, which structural typing won't force.
150
194
  target?: {
151
195
  testFile?: string;
152
196
  sessionId?: string;
@@ -1,3 +1,4 @@
1
+ import type { BrowserProvider } from '@rstest/core/internal/browser';
1
2
  import type { BrowserRpcRequest } from '../rpcProtocol';
2
3
  import { playwrightProviderImplementation } from './playwright';
3
4
 
@@ -14,8 +15,14 @@ import { playwrightProviderImplementation } from './playwright';
14
15
  * - do not reference optional peer provider types from public declarations
15
16
  * - keep provider-specific behavior and config decoding inside provider implementations
16
17
  * - prefer direct passthrough to provider APIs over provider-specific translation
18
+ *
19
+ * The provider-name union is owned by `@rstest/core` (see `BROWSER_PROVIDERS`)
20
+ * because core's CLI `init` templates need it but cannot import this package
21
+ * (peer-dependency direction). `providerImplementations` below is keyed by the
22
+ * re-exported union, so a provider added in core without an implementation here
23
+ * is a compile error.
17
24
  */
18
- export type BrowserProvider = 'playwright';
25
+ export type { BrowserProvider };
19
26
 
20
27
  /** Minimal console shape needed by host logging bridge. */
21
28
  export type BrowserConsoleMessage = {
@@ -178,28 +178,41 @@ export const validateBrowserRpcRequest = (
178
178
  return request;
179
179
  };
180
180
 
181
+ /**
182
+ * Single source of truth for snapshot RPC methods and their argument shapes.
183
+ * The request union, the client sender, both host-side mappers, and every
184
+ * exhaustiveness check derive from this map — adding or renaming a method here
185
+ * forces a compile error at every site that has not been updated.
186
+ */
187
+ export type SnapshotRpcMethodArgs = {
188
+ resolveSnapshotPath: { testPath: string };
189
+ readSnapshotFile: { filepath: string };
190
+ saveSnapshotFile: { filepath: string; content: string };
191
+ removeSnapshotFile: { filepath: string };
192
+ };
193
+
194
+ export type SnapshotRpcMethod = keyof SnapshotRpcMethodArgs;
195
+
181
196
  /**
182
197
  * Snapshot RPC request from runner iframe.
183
198
  * The container will forward these to the host via WebSocket RPC.
184
199
  */
185
- export type SnapshotRpcRequest =
186
- | {
187
- id: string;
188
- method: 'resolveSnapshotPath';
189
- args: { testPath: string };
190
- }
191
- | {
192
- id: string;
193
- method: 'readSnapshotFile';
194
- args: { filepath: string };
195
- }
196
- | {
197
- id: string;
198
- method: 'saveSnapshotFile';
199
- args: { filepath: string; content: string };
200
- }
201
- | {
202
- id: string;
203
- method: 'removeSnapshotFile';
204
- args: { filepath: string };
205
- };
200
+ export type SnapshotRpcRequest = {
201
+ [M in SnapshotRpcMethod]: {
202
+ id: string;
203
+ method: M;
204
+ args: SnapshotRpcMethodArgs[M];
205
+ };
206
+ }[SnapshotRpcMethod];
207
+
208
+ /**
209
+ * Client-side snapshot RPC call (request without the transport `id`).
210
+ * Distributive over {@link SnapshotRpcMethod} so `method` and `args` stay paired
211
+ * at the call site — a mismatched pair fails to compile.
212
+ */
213
+ export type SnapshotRpcCall = {
214
+ [M in SnapshotRpcMethod]: {
215
+ method: M;
216
+ args: SnapshotRpcMethodArgs[M];
217
+ };
218
+ }[SnapshotRpcMethod];
@@ -4,6 +4,11 @@
4
4
  * IMPORTANT: Keep this list/map in sync with `DevicePreset` typing in
5
5
  * `@rstest/core` (`packages/core/src/types/config.ts`) so `defineConfig`
6
6
  * autocomplete and runtime validation stay consistent.
7
+ *
8
+ * Upstream source of truth for ids/dimensions is the Chrome DevTools emulated
9
+ * device list — mirror each device's `screen.vertical` `{ width, height }`:
10
+ * https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/models/emulation/EmulatedDevices.ts
11
+ * Last verified in sync (all 17): 2026-04-22, devtools-frontend commit abaac05e.
7
12
  */
8
13
  export const BROWSER_VIEWPORT_PRESET_IDS = [
9
14
  'iPhoneSE',
@@ -1,8 +1,6 @@
1
- import { color, logger } from '@rstest/core/browser';
1
+ import { color, isTTY, logger } from '@rstest/core/internal/browser';
2
2
 
3
- const isTTY = (): boolean => Boolean(process.stdin.isTTY && !process.env.CI);
4
-
5
- export const isBrowserWatchCliShortcutsEnabled = (): boolean => isTTY();
3
+ export const isBrowserWatchCliShortcutsEnabled = (): boolean => isTTY('stdin');
6
4
 
7
5
  export const getBrowserWatchCliShortcutsHintMessage = (): string => {
8
6
  return ` ${color.dim('press')} ${color.bold('q')} ${color.dim('to quit')}\n`;
@@ -1 +0,0 @@
1
- @import "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap";@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-zinc-950:#09090b;--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--tracking-widest:.1em;--radius-md:.375rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}@supports (color:lab(0% 0 0)){:root,:host{--color-zinc-950:lab(2.51107% .242703 -.886115)}}}@layer base,components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing) * 0)}.mt-3{margin-top:calc(var(--spacing) * 3)}.ml-1{margin-left:calc(var(--spacing) * 1)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.h-2{height:calc(var(--spacing) * 2)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-\[1px\]{height:1px}.h-\[48px\]{height:48px}.h-full{height:100%}.h-screen{height:100vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-2{width:calc(var(--spacing) * 2)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-\[16px\]{width:16px}.w-\[120px\]{width:120px}.w-\[280px\]{width:280px}.w-full{width:100%}.max-w-\[400px\]{max-width:400px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.resize{resize:both}.grid-cols-\[auto_minmax\(0\,1fr\)_auto\]{grid-template-columns:auto minmax(0,1fr) auto}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.bg-black\/\[0\.02\]{background-color:#00000005}@supports (color:color-mix(in lab, red, red)){.bg-black\/\[0\.02\]{background-color:color-mix(in oklab, var(--color-black) 2%, transparent)}}.bg-transparent{background-color:#0000}.bg-zinc-950{background-color:var(--color-zinc-950)}.p-0{padding:calc(var(--spacing) * 0)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0{padding-block:calc(var(--spacing) * 0)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-\(--accents-3\){color:var(--accents-3)}.text-\(--accents-4\){color:var(--accents-4)}.text-\(--accents-5\){color:var(--accents-5)}.text-\(--accents-6\){color:var(--accents-6)}.text-\(--accents-7\){color:var(--accents-7)}.text-\(--muted-foreground\){color:var(--muted-foreground)}.text-\(--warning\){color:var(--warning)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, )}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-\[cubic-bezier\(0\.4\,0\,0\.2\,1\)\]{--tw-ease:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(.4,0,.2,1)}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:bg-\(--accents-1\):hover{background-color:var(--accents-1)}.hover\:text-\(--accents-6\):hover{color:var(--accents-6)}}}.ant-typography.font-mono{font-family:var(--font-mono)}:root{--font-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--background:#fff;--foreground:#000;--ds-gray-100:#fafafa;--ds-gray-200:#eaeaea;--ds-gray-300:#999;--ds-gray-400:#888;--ds-gray-500:#666;--ds-gray-600:#444;--ds-gray-700:#333;--ds-gray-800:#111;--ds-gray-900:#000;--ds-gray-1000:#000;--ds-red-100:#fff0f0;--ds-red-200:#ffebeb;--ds-red-300:#ffe6e6;--ds-red-700:#e5484d;--ds-red-800:#da2f35;--ds-red-900:#cb2a2f;--ds-green-100:#effbef;--ds-green-200:#ebfaeb;--ds-green-300:#daf6da;--ds-green-700:#45a557;--ds-green-800:#398e4a;--ds-green-900:#297a3a;--ds-amber-100:#fff6e6;--ds-amber-200:#fff4d6;--ds-amber-300:#fef0cd;--ds-amber-700:#ffb224;--ds-amber-800:#ff990a;--ds-amber-900:#a35200;--ds-blue-700:#0070f3;--accents-1:var(--ds-gray-100);--accents-2:var(--ds-gray-200);--accents-3:var(--ds-gray-300);--accents-4:var(--ds-gray-400);--accents-5:var(--ds-gray-500);--accents-6:var(--ds-gray-600);--accents-7:var(--ds-gray-700);--accents-8:var(--ds-gray-800);--border:var(--accents-2);--muted:var(--accents-1);--muted-foreground:var(--accents-5);--primary:var(--foreground);--success:var(--ds-green-700);--error:var(--ds-red-800);--warning:var(--ds-amber-700);--lightningcss-light:initial;--lightningcss-dark: ;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}[data-theme=dark]{--background:#000;--foreground:#fff;--ds-gray-100:#111;--ds-gray-200:#333;--ds-gray-300:#444;--ds-gray-400:#666;--ds-gray-500:#888;--ds-gray-600:#999;--ds-gray-700:#eaeaea;--ds-gray-800:#fafafa;--ds-gray-900:#fff;--ds-gray-1000:#fff;--ds-red-100:#2a1314;--ds-red-200:#3c1618;--ds-red-300:#561a1e;--ds-red-700:#e5484d;--ds-red-800:#d93036;--ds-red-900:#ff6166;--ds-green-100:#0b2212;--ds-green-200:#0f2e18;--ds-green-300:#12361b;--ds-green-700:#45a557;--ds-green-800:#398e4a;--ds-green-900:#62c073;--ds-amber-100:#291800;--ds-amber-200:#331b00;--ds-amber-300:#4d2a00;--ds-amber-700:#ffb224;--ds-amber-800:#ff990a;--ds-amber-900:#f2a20d;--ds-blue-700:#0070f3;--accents-1:var(--ds-gray-100);--accents-2:var(--ds-gray-200);--accents-3:var(--ds-gray-300);--accents-4:var(--ds-gray-400);--accents-5:var(--ds-gray-500);--accents-6:var(--ds-gray-600);--accents-7:var(--ds-gray-700);--accents-8:var(--ds-gray-800);--border:var(--accents-2);--muted:var(--accents-1);--muted-foreground:var(--accents-5);--primary:var(--foreground);--success:var(--ds-green-700);--error:var(--ds-red-800);--warning:var(--ds-amber-700);--lightningcss-light: ;--lightningcss-dark:initial;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}body{background:var(--background);min-height:100vh;color:var(--foreground);margin:0;font-family:Inter,system-ui,-apple-system,sans-serif;overflow:hidden}.ant-input-affix-wrapper:focus,.ant-input-affix-wrapper-focused{border-color:var(--foreground);box-shadow:0 0 0 1px var(--foreground)}.ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected{transition:none}@keyframes status-flash{0%{filter:brightness()}5%{filter:brightness(2.5)}15%{filter:brightness(1.8)}40%{filter:brightness(1.3)}to{filter:brightness()}}.status-icon-flash{animation:1.5s ease-out status-flash}@keyframes progress-segment-flash{0%{filter:brightness();box-shadow:none}10%{filter:brightness(2.5);box-shadow:0 0 8px 1px}to{filter:brightness();box-shadow:none}}.progress-flash-active{animation:1s cubic-bezier(.4,0,.2,1) progress-segment-flash}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}
@@ -1 +0,0 @@
1
- /*! LICENSE: 27.099feaf1da.js.LICENSE.txt */
@@ -1 +0,0 @@
1
- /*! LICENSE: lib-react.5ae9b90ee5.js.LICENSE.txt */
@@ -1,4 +0,0 @@
1
- declare global {
2
- var __coverage__: Record<string, unknown> | undefined;
3
- }
4
- export {};