@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
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 = {
@@ -44,6 +51,7 @@ export type BrowserProviderPage = {
44
51
  ): void;
45
52
  };
46
53
  close: () => Promise<void>;
54
+ [Symbol.asyncDispose]: () => Promise<void>;
47
55
  };
48
56
 
49
57
  /** Minimal browser context API surface required by hostController. */
@@ -51,11 +59,13 @@ export type BrowserProviderContext = {
51
59
  newPage: () => Promise<BrowserProviderPage>;
52
60
  on: (event: 'page', listener: (page: BrowserProviderPage) => void) => void;
53
61
  close: () => Promise<void>;
62
+ [Symbol.asyncDispose]: () => Promise<void>;
54
63
  };
55
64
 
56
65
  /** Minimal browser API surface required by hostController. */
57
66
  export type BrowserProviderBrowser = {
58
67
  close: () => Promise<void>;
68
+ [Symbol.asyncDispose]: () => Promise<void>;
59
69
  newContext: (options: {
60
70
  viewport: { width: number; height: number } | null;
61
71
  providerOptions?: Record<string, unknown>;
@@ -1,4 +1,104 @@
1
- import type { BrowserProviderContext, BrowserProviderRuntime } from '../index';
1
+ import type {
2
+ BrowserConsoleMessage,
3
+ BrowserProviderContext,
4
+ BrowserProviderPage,
5
+ BrowserProviderRuntime,
6
+ } from '../index';
7
+
8
+ type PlaywrightPageLike = Omit<
9
+ BrowserProviderPage,
10
+ 'on' | typeof Symbol.asyncDispose
11
+ > & {
12
+ on: {
13
+ (event: 'popup', listener: (page: PlaywrightPageLike) => void): void;
14
+ (
15
+ event: 'console',
16
+ listener: (message: BrowserConsoleMessage) => void,
17
+ ): void;
18
+ };
19
+ };
20
+ type PlaywrightContextLike = Omit<
21
+ BrowserProviderContext,
22
+ 'newPage' | typeof Symbol.asyncDispose
23
+ > & {
24
+ newPage: () => Promise<PlaywrightPageLike>;
25
+ };
26
+
27
+ type AsyncDisposableResource<T> = T & {
28
+ [Symbol.asyncDispose]: () => Promise<void>;
29
+ };
30
+
31
+ type PlaywrightBrowserLike = Omit<
32
+ BrowserProviderRuntime['browser'],
33
+ typeof Symbol.asyncDispose
34
+ >;
35
+
36
+ const patchedPages = new WeakSet<PlaywrightPageLike>();
37
+ const patchedContexts = new WeakSet<PlaywrightContextLike>();
38
+
39
+ const withAsyncDispose = <T extends { close: () => Promise<void> }>(
40
+ resource: T,
41
+ ): AsyncDisposableResource<T> => {
42
+ if (typeof Symbol.asyncDispose === 'symbol') {
43
+ const existing = Object.getOwnPropertyDescriptor(
44
+ resource,
45
+ Symbol.asyncDispose,
46
+ );
47
+ if (!existing) {
48
+ Object.defineProperty(resource, Symbol.asyncDispose, {
49
+ configurable: true,
50
+ value: () => resource.close(),
51
+ });
52
+ }
53
+ }
54
+
55
+ return resource as AsyncDisposableResource<T>;
56
+ };
57
+
58
+ const wrapPage = (page: PlaywrightPageLike): BrowserProviderPage => {
59
+ if (patchedPages.has(page)) {
60
+ return withAsyncDispose(page) as BrowserProviderPage;
61
+ }
62
+
63
+ patchedPages.add(page);
64
+ const originalOn = page.on.bind(page);
65
+ page.on = ((
66
+ event: 'popup' | 'console',
67
+ listener:
68
+ | ((page: BrowserProviderPage) => void)
69
+ | ((message: BrowserConsoleMessage) => void),
70
+ ) => {
71
+ if (event === 'popup') {
72
+ originalOn(event, (popup) => {
73
+ (listener as (page: BrowserProviderPage) => void)(wrapPage(popup));
74
+ });
75
+ return;
76
+ }
77
+
78
+ originalOn(event, listener as (message: BrowserConsoleMessage) => void);
79
+ }) as PlaywrightPageLike['on'];
80
+
81
+ return withAsyncDispose(page) as BrowserProviderPage;
82
+ };
83
+
84
+ const wrapContext = (
85
+ context: PlaywrightContextLike,
86
+ ): BrowserProviderContext => {
87
+ if (patchedContexts.has(context)) {
88
+ return withAsyncDispose(context) as BrowserProviderContext;
89
+ }
90
+
91
+ patchedContexts.add(context);
92
+ const originalNewPage = context.newPage.bind(context);
93
+ context.newPage = async () => wrapPage(await originalNewPage());
94
+
95
+ const originalOn = context.on.bind(context);
96
+ context.on = (event, listener) => {
97
+ originalOn(event, (page) => listener(wrapPage(page)));
98
+ };
99
+
100
+ return withAsyncDispose(context) as BrowserProviderContext;
101
+ };
2
102
 
3
103
  export async function launchPlaywrightBrowser({
4
104
  browserName,
@@ -30,7 +130,7 @@ export async function launchPlaywrightBrowser({
30
130
  args: launchArgs,
31
131
  });
32
132
 
33
- const wrappedBrowser: BrowserProviderRuntime['browser'] = {
133
+ const wrappedBrowser = withAsyncDispose({
34
134
  close: async () => browser.close(),
35
135
  newContext: async ({
36
136
  providerOptions: contextProviderOptions,
@@ -44,9 +144,9 @@ export async function launchPlaywrightBrowser({
44
144
  viewport,
45
145
  });
46
146
 
47
- return context as unknown as BrowserProviderContext;
147
+ return wrapContext(context as unknown as PlaywrightContextLike);
48
148
  },
49
- };
149
+ } satisfies PlaywrightBrowserLike) as BrowserProviderRuntime['browser'];
50
150
 
51
151
  return {
52
152
  browser: wrappedBrowser,
@@ -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
- /*! LICENSE: 188.480c3c49b5.js.LICENSE.txt */
@@ -1 +0,0 @@
1
- /*! LICENSE: lib-react.5ae9b90ee5.js.LICENSE.txt */