@rstest/browser 0.8.5 → 0.9.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 (84) hide show
  1. package/LICENSE-APACHE-2.0 +202 -0
  2. package/NOTICE +11 -0
  3. package/dist/361.js +8 -0
  4. package/dist/augmentExpect.d.ts +73 -0
  5. package/dist/browser-container/container-static/css/index.5c72297783.css +1 -0
  6. package/dist/browser-container/container-static/js/{565.226c9ef5.js → 101.36a8ccdf84.js} +4024 -3856
  7. package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +1 -0
  8. package/dist/browser-container/container-static/js/{index.c1d17467.js → index.28d833de0b.js} +732 -675
  9. package/dist/browser-container/container-static/js/{lib-react.97ee79b0.js → lib-react.dcf2a5e57a.js} +10 -10
  10. package/dist/browser-container/container-static/js/lib-react.dcf2a5e57a.js.LICENSE.txt +1 -0
  11. package/dist/browser-container/index.html +1 -1
  12. package/dist/browser.d.ts +2 -0
  13. package/dist/browser.js +583 -0
  14. package/dist/browserRpcRegistry.d.ts +18 -0
  15. package/dist/client/api.d.ts +3 -0
  16. package/dist/client/browserRpc.d.ts +2 -0
  17. package/dist/client/dispatchTransport.d.ts +11 -0
  18. package/dist/client/entry.d.ts +1 -5
  19. package/dist/client/locator.d.ts +125 -0
  20. package/dist/client/snapshot.d.ts +0 -6
  21. package/dist/concurrency.d.ts +12 -0
  22. package/dist/dispatchCapabilities.d.ts +34 -0
  23. package/dist/dispatchRouter.d.ts +20 -0
  24. package/dist/headedSerialTaskQueue.d.ts +8 -0
  25. package/dist/headlessLatestRerunScheduler.d.ts +19 -0
  26. package/dist/headlessTransport.d.ts +12 -0
  27. package/dist/hostController.d.ts +16 -0
  28. package/dist/index.js +1790 -296
  29. package/dist/protocol.d.ts +44 -33
  30. package/dist/providers/index.d.ts +79 -0
  31. package/dist/providers/playwright/compileLocator.d.ts +3 -0
  32. package/dist/providers/playwright/dispatchBrowserRpc.d.ts +13 -0
  33. package/dist/providers/playwright/expectUtils.d.ts +24 -0
  34. package/dist/providers/playwright/implementation.d.ts +2 -0
  35. package/dist/providers/playwright/index.d.ts +1 -0
  36. package/dist/providers/playwright/runtime.d.ts +5 -0
  37. package/dist/providers/playwright/textMatcher.d.ts +8 -0
  38. package/dist/rpcProtocol.d.ts +145 -0
  39. package/dist/runSession.d.ts +33 -0
  40. package/dist/sessionRegistry.d.ts +34 -0
  41. package/dist/sourceMap/sourceMapLoader.d.ts +14 -0
  42. package/dist/watchCliShortcuts.d.ts +6 -0
  43. package/dist/watchRerunPlanner.d.ts +21 -0
  44. package/package.json +17 -12
  45. package/src/AGENTS.md +128 -0
  46. package/src/augmentExpect.ts +62 -0
  47. package/src/browser.ts +3 -0
  48. package/src/browserRpcRegistry.ts +57 -0
  49. package/src/client/AGENTS.md +82 -0
  50. package/src/client/api.ts +213 -0
  51. package/src/client/browserRpc.ts +86 -0
  52. package/src/client/dispatchTransport.ts +178 -0
  53. package/src/client/entry.ts +96 -33
  54. package/src/client/locator.ts +452 -0
  55. package/src/client/snapshot.ts +32 -97
  56. package/src/client/sourceMapSupport.ts +26 -37
  57. package/src/concurrency.ts +62 -0
  58. package/src/dispatchCapabilities.ts +162 -0
  59. package/src/dispatchRouter.ts +82 -0
  60. package/src/env.d.ts +8 -1
  61. package/src/headedSerialTaskQueue.ts +19 -0
  62. package/src/headlessLatestRerunScheduler.ts +76 -0
  63. package/src/headlessTransport.ts +28 -0
  64. package/src/hostController.ts +1538 -384
  65. package/src/protocol.ts +66 -31
  66. package/src/providers/index.ts +103 -0
  67. package/src/providers/playwright/compileLocator.ts +130 -0
  68. package/src/providers/playwright/dispatchBrowserRpc.ts +372 -0
  69. package/src/providers/playwright/expectUtils.ts +57 -0
  70. package/src/providers/playwright/implementation.ts +33 -0
  71. package/src/providers/playwright/index.ts +1 -0
  72. package/src/providers/playwright/runtime.ts +32 -0
  73. package/src/providers/playwright/textMatcher.ts +10 -0
  74. package/src/rpcProtocol.ts +220 -0
  75. package/src/runSession.ts +110 -0
  76. package/src/sessionRegistry.ts +89 -0
  77. package/src/sourceMap/sourceMapLoader.ts +96 -0
  78. package/src/watchCliShortcuts.ts +77 -0
  79. package/src/watchRerunPlanner.ts +77 -0
  80. package/dist/browser-container/container-static/css/index.5a71c757.css +0 -1
  81. package/dist/browser-container/container-static/js/565.226c9ef5.js.LICENSE.txt +0 -1
  82. package/dist/browser-container/container-static/js/lib-react.97ee79b0.js.LICENSE.txt +0 -1
  83. package/dist/browser-container/container-static/js/scheduler.5accca0c.js +0 -407
  84. package/dist/browser-container/scheduler.html +0 -19
package/src/AGENTS.md ADDED
@@ -0,0 +1,128 @@
1
+ # Browser mode host architecture
2
+
3
+ This document is architecture-only and focuses on browser mode scheduling in `@rstest/browser` host-side modules.
4
+
5
+ ## Module topology
6
+
7
+ ```mermaid
8
+ flowchart LR
9
+ subgraph Host["@rstest/browser host (Node.js)"]
10
+ IDX["index.ts\nrunBrowserTests()"]
11
+ HC["hostController.ts\nrunBrowserController()"]
12
+ RT["createBrowserRuntime()"]
13
+ DR["dispatchCapabilities.ts + dispatchRouter.ts\nnamespace router"]
14
+ RL["runSession.ts\nRunSessionLifecycle"]
15
+ SR["sessionRegistry.ts\nRunnerSessionRegistry"]
16
+ WP["watchRerunPlanner.ts"]
17
+ LS["headlessLatestRerunScheduler.ts"]
18
+ end
19
+
20
+ subgraph UI["@rstest/browser-ui container (headed path)"]
21
+ UR["useRpc() / birpc"]
22
+ CH["core/channel.ts\nforwardDispatchRpcRequest()"]
23
+ MH["main.tsx message listener\nforward lifecycle callbacks"]
24
+ end
25
+
26
+ subgraph Runner["runner runtime (src/client/entry.ts)"]
27
+ MSG["runner lifecycle messages"]
28
+ RPC["dispatch-rpc-request"]
29
+ end
30
+
31
+ IDX --> HC
32
+ HC --> RT
33
+ HC --> DR
34
+ HC --> WP
35
+ WP --> LS
36
+ HC --> RL
37
+ RL --> SR
38
+
39
+ UR <--> HC
40
+ MSG --> MH
41
+ MH --> UR
42
+ RPC --> CH
43
+ CH --> UR
44
+
45
+ HC -."headless bridge:\nexposeFunction(\_\_rstest_dispatch\_\_, \_\_rstest_dispatch_rpc\_\_)".-> Runner
46
+ ```
47
+
48
+ ## Headed transport path
49
+
50
+ Primary dispatch request direction is `Runner -> Container -> Host -> Router -> Handler`.
51
+ `Host -> Container` in this path is bootstrap setup and callback delivery, not router request initiation.
52
+ `dispatchRouter` handles inbound request routing only; outbound response delivery is a transport reply.
53
+
54
+ ### Bootstrap control plane
55
+
56
+ ```mermaid
57
+ sequenceDiagram
58
+ participant Host as browser hostController
59
+ participant Container as browser-ui container
60
+
61
+ Host->>Container: open container and establish birpc
62
+ Host->>Container: provide BrowserHostConfig
63
+ Container-->>Host: getTestFiles and rerun requests
64
+ ```
65
+
66
+ ### Runtime dispatch RPC data plane
67
+
68
+ ```mermaid
69
+ sequenceDiagram
70
+ participant Runner as client iframe runner
71
+ participant Container as browser-ui channel
72
+ participant Host as browser hostController
73
+ participant Router as browser dispatchRouter
74
+ participant Handler as namespace handler
75
+
76
+ Runner->>Container: postMessage runner lifecycle
77
+ Container->>Host: forward lifecycle callbacks (onTest*)
78
+
79
+ Runner->>Container: postMessage dispatch rpc request
80
+ Container->>Host: rpc.dispatch(request)
81
+ Host->>Router: routing inbound request
82
+ Router->>Handler: resolve namespace handler
83
+ Handler->>Host: execute host capability work
84
+ Host-->>Handler: capability result or error
85
+ Handler-->>Router: return handler result
86
+ Router-->>Host: routing done, response payload
87
+ Host-->>Container: transport reply payload
88
+ Container-->>Runner: transport reply to runner
89
+ ```
90
+
91
+ ## Headless transport path
92
+
93
+ Primary dispatch request direction is `Runner -> Host -> Router -> Handler`.
94
+ `Host -> Runner` in this path is bridge registration, not router request initiation.
95
+ `dispatchRouter` handles inbound request routing only; outbound response delivery is a transport reply.
96
+
97
+ ### Bootstrap control plane
98
+
99
+ ```mermaid
100
+ sequenceDiagram
101
+ participant Host as browser hostController
102
+ participant Runner as client top level runner
103
+
104
+ Host->>Runner: exposeFunction __rstest_dispatch__
105
+ Host->>Runner: exposeFunction __rstest_dispatch_rpc__
106
+ ```
107
+
108
+ ### Runtime dispatch RPC data plane
109
+
110
+ ```mermaid
111
+ sequenceDiagram
112
+ participant Runner as client top level runner
113
+ participant Host as browser hostController
114
+ participant Router as browser dispatchRouter
115
+ participant Handler as namespace handler
116
+
117
+ Runner->>Host: __rstest_dispatch__ lifecycle
118
+ Host->>Host: forward lifecycle callbacks
119
+
120
+ Runner->>Host: __rstest_dispatch_rpc__ request
121
+ Host->>Router: routing inbound request
122
+ Router->>Handler: resolve namespace handler
123
+ Handler->>Host: execute host capability work
124
+ Host-->>Handler: capability result or error
125
+ Handler-->>Router: return handler result
126
+ Router-->>Host: routing done, response payload
127
+ Host-->>Runner: transport reply payload
128
+ ```
@@ -0,0 +1,62 @@
1
+ import type { Locator } from './client/locator';
2
+
3
+ export type BrowserElementExpect = {
4
+ not: BrowserElementExpect;
5
+ toBeVisible: (options?: { timeout?: number }) => Promise<void>;
6
+ toBeHidden: (options?: { timeout?: number }) => Promise<void>;
7
+ toBeEnabled: (options?: { timeout?: number }) => Promise<void>;
8
+ toBeDisabled: (options?: { timeout?: number }) => Promise<void>;
9
+ toBeChecked: (options?: { timeout?: number }) => Promise<void>;
10
+ toBeUnchecked: (options?: { timeout?: number }) => Promise<void>;
11
+ toBeAttached: (options?: { timeout?: number }) => Promise<void>;
12
+ toBeDetached: (options?: { timeout?: number }) => Promise<void>;
13
+ toBeEditable: (options?: { timeout?: number }) => Promise<void>;
14
+ toBeFocused: (options?: { timeout?: number }) => Promise<void>;
15
+ toBeEmpty: (options?: { timeout?: number }) => Promise<void>;
16
+ toBeInViewport: (options?: {
17
+ timeout?: number;
18
+ ratio?: number;
19
+ }) => Promise<void>;
20
+ toHaveText: (
21
+ text: string | RegExp,
22
+ options?: { timeout?: number },
23
+ ) => Promise<void>;
24
+ toContainText: (
25
+ text: string | RegExp,
26
+ options?: { timeout?: number },
27
+ ) => Promise<void>;
28
+ toHaveValue: (
29
+ value: string | RegExp,
30
+ options?: { timeout?: number },
31
+ ) => Promise<void>;
32
+ toHaveId: (
33
+ value: string | RegExp,
34
+ options?: { timeout?: number },
35
+ ) => Promise<void>;
36
+ toHaveAttribute: (
37
+ name: string,
38
+ value?: string | RegExp,
39
+ options?: { timeout?: number },
40
+ ) => Promise<void>;
41
+ toHaveClass: (
42
+ value: string | RegExp,
43
+ options?: { timeout?: number },
44
+ ) => Promise<void>;
45
+ toHaveCount: (count: number, options?: { timeout?: number }) => Promise<void>;
46
+ toHaveCSS: (
47
+ name: string,
48
+ value: string | RegExp,
49
+ options?: { timeout?: number },
50
+ ) => Promise<void>;
51
+ toHaveJSProperty: (
52
+ name: string,
53
+ value: unknown,
54
+ options?: { timeout?: number },
55
+ ) => Promise<void>;
56
+ };
57
+
58
+ declare module '@rstest/core' {
59
+ interface ExpectStatic {
60
+ element: (locator: Locator) => BrowserElementExpect;
61
+ }
62
+ }
package/src/browser.ts ADDED
@@ -0,0 +1,3 @@
1
+ import './augmentExpect';
2
+
3
+ export * from './client/api';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Runtime allowlists for Browser RPC methods.
3
+ *
4
+ * Planned capabilities are intentionally documented in comments (not runtime
5
+ * data) to keep this module focused on host-side validation.
6
+ *
7
+ * Planned gaps (non-exhaustive):
8
+ * - Locator query/interop: filter({ hasNot, hasNotText }), locator.selector/length,
9
+ * locator.query()/element()/elements()/all(), page.elementLocator(element),
10
+ * locators.extend(...)
11
+ * - Locator actions: tripleClick, hover out, drag/drop helpers
12
+ * - Assertions: a11y matchers (accessible name/description), toHaveRole,
13
+ * toHaveValues
14
+ * - Artifacts intentionally excluded for now: screenshot/toMatchScreenshot/
15
+ * trace/video
16
+ */
17
+ export const supportedLocatorActions = new Set<string>([
18
+ 'click',
19
+ 'dblclick',
20
+ 'fill',
21
+ 'hover',
22
+ 'press',
23
+ 'clear',
24
+ 'check',
25
+ 'uncheck',
26
+ 'focus',
27
+ 'blur',
28
+ 'scrollIntoViewIfNeeded',
29
+ 'waitFor',
30
+ 'dispatchEvent',
31
+ 'selectOption',
32
+ 'setInputFiles',
33
+ ]);
34
+
35
+ export const supportedExpectElementMatchers = new Set<string>([
36
+ 'toBeVisible',
37
+ 'toBeHidden',
38
+ 'toBeEnabled',
39
+ 'toBeDisabled',
40
+ 'toBeAttached',
41
+ 'toBeDetached',
42
+ 'toBeEditable',
43
+ 'toBeFocused',
44
+ 'toBeEmpty',
45
+ 'toBeInViewport',
46
+ 'toHaveText',
47
+ 'toContainText',
48
+ 'toHaveValue',
49
+ 'toHaveAttribute',
50
+ 'toHaveClass',
51
+ 'toHaveCount',
52
+ 'toBeChecked',
53
+ 'toBeUnchecked',
54
+ 'toHaveId',
55
+ 'toHaveCSS',
56
+ 'toHaveJSProperty',
57
+ ]);
@@ -0,0 +1,82 @@
1
+ # Browser mode runner architecture
2
+
3
+ This document is architecture-only and focuses on the browser runner runtime in `src/client`.
4
+
5
+ ## Runner bootstrap pipeline
6
+
7
+ ```mermaid
8
+ flowchart TD
9
+ A["waitForConfig()"] --> B["read \_\_RSTEST_BROWSER_OPTIONS\_\_ + URL overrides"]
10
+ B --> R["send ready"]
11
+ R --> C["setRealTimers()"]
12
+ C --> D["preloadRunnerSourceMap()"]
13
+ D --> E["resolve project + runtimeConfig"]
14
+ E --> F{"execution mode"}
15
+
16
+ F -->|collect| G["create runtime + load setup/test modules + runner.collectTests()"]
17
+ G --> H["send collect-result / collect-complete"]
18
+
19
+ F -->|run| I["interceptConsole() + createRstestRuntime()"]
20
+ I --> J["send file-start"]
21
+ J --> K["load setup files + load test module"]
22
+ K --> L["runner.runTests() + send case-result"]
23
+ L --> N["send file-complete per file"]
24
+ N --> O["send complete after all files"]
25
+
26
+ H --> M["window.\_\_RSTEST_DONE\_\_ = true"]
27
+ O --> M
28
+ ```
29
+
30
+ ## Transport architecture
31
+
32
+ ```mermaid
33
+ flowchart LR
34
+ subgraph IframePath["Iframe path (headed)"]
35
+ S1["send()"] --> P1["parent.postMessage(\_\_rstest_dispatch\_\_)"]
36
+ R1["dispatchRunnerLifecycle()"] --> P2["postMessage dispatch-rpc-request"]
37
+ SN1["snapshot.sendRpcRequest()"] --> P3["postMessage dispatch-rpc-request + wait \_\_rstest_dispatch_response\_\_"]
38
+ end
39
+
40
+ subgraph TopLevelRunPath["Top-level page path (headless run)"]
41
+ S2["send()"] --> D1["window.\_\_rstest_dispatch\_\_"]
42
+ R2["dispatchRunnerLifecycle()"] --> D2["window.\_\_rstest_dispatch_rpc\_\_"]
43
+ SN2["snapshot.sendRpcRequest()"] --> D3["window.\_\_rstest_dispatch_rpc\_\_"]
44
+ end
45
+
46
+ subgraph TopLevelCollectPath["Top-level page path (list collect)"]
47
+ S3["send()"] --> C1["window.\_\_rstest_dispatch\_\_"]
48
+ end
49
+ ```
50
+
51
+ ## Snapshot RPC sequence
52
+
53
+ ```mermaid
54
+ sequenceDiagram
55
+ participant Snap as snapshot.ts
56
+ participant Runner as entry.ts runtime
57
+ participant Container as browser-ui channel
58
+ participant Host as host dispatch router
59
+
60
+ Snap->>Runner: sendRpcRequest(method, args)
61
+
62
+ alt top-level runner (headless run)
63
+ Runner->>Host: __rstest_dispatch_rpc__(namespace=snapshot)
64
+ Host-->>Runner: BrowserDispatchResponse
65
+ Runner-->>Snap: result/error
66
+ else iframe runner (headed)
67
+ Runner->>Container: postMessage(dispatch-rpc-request)
68
+ Container->>Host: rpc.dispatch(request)
69
+ Host-->>Container: BrowserDispatchResponse
70
+ Container-->>Runner: __rstest_dispatch_response__
71
+ Runner-->>Snap: resolve/reject pending request
72
+ end
73
+ ```
74
+
75
+ List collect mode does not use the snapshot RPC namespace.
76
+
77
+ ## Runtime invariants
78
+
79
+ - `entry.ts` is the only bootstrap entry and decides `collect` vs `run` mode.
80
+ - Runner lifecycle events (`file-ready`, `suite-start`, `suite-result`, `case-start`) go through the `runner` dispatch namespace.
81
+ - Snapshot file operations go through the `snapshot` dispatch namespace and never access filesystem directly in browser runtime.
82
+ - Console interception is per test file and must restore original console methods in `finally`.
@@ -0,0 +1,213 @@
1
+ import type { BrowserElementExpect } from '../augmentExpect';
2
+ import type { BrowserLocatorText, BrowserRpcRequest } from '../rpcProtocol';
3
+ import { callBrowserRpc } from './browserRpc';
4
+ import {
5
+ isLocator,
6
+ Locator,
7
+ page,
8
+ serializeText,
9
+ setTestIdAttribute,
10
+ } from './locator';
11
+
12
+ const serializeMatcherText = (value: string | RegExp): BrowserLocatorText => {
13
+ return serializeText(value);
14
+ };
15
+
16
+ const createElementExpect = (
17
+ locator: Locator,
18
+ isNot: boolean,
19
+ ): BrowserElementExpect => {
20
+ const callExpect = async (
21
+ method: string,
22
+ args: unknown[],
23
+ timeout?: number,
24
+ ): Promise<void> => {
25
+ await callBrowserRpc<void>({
26
+ kind: 'expect',
27
+ locator: locator.ir,
28
+ method,
29
+ args,
30
+ isNot,
31
+ timeout,
32
+ } satisfies Omit<BrowserRpcRequest, 'id' | 'testPath' | 'runId'>);
33
+ };
34
+
35
+ const api: Omit<BrowserElementExpect, 'not'> = {
36
+ async toBeVisible(options) {
37
+ await callExpect('toBeVisible', [], options?.timeout);
38
+ },
39
+ async toBeHidden(options) {
40
+ await callExpect('toBeHidden', [], options?.timeout);
41
+ },
42
+ async toBeEnabled(options) {
43
+ await callExpect('toBeEnabled', [], options?.timeout);
44
+ },
45
+ async toBeDisabled(options) {
46
+ await callExpect('toBeDisabled', [], options?.timeout);
47
+ },
48
+ async toBeChecked(options) {
49
+ await callExpect('toBeChecked', [], options?.timeout);
50
+ },
51
+ async toBeUnchecked(options) {
52
+ await callExpect('toBeUnchecked', [], options?.timeout);
53
+ },
54
+ async toBeAttached(options) {
55
+ await callExpect('toBeAttached', [], options?.timeout);
56
+ },
57
+ async toBeDetached(options) {
58
+ await callExpect('toBeDetached', [], options?.timeout);
59
+ },
60
+ async toBeEditable(options) {
61
+ await callExpect('toBeEditable', [], options?.timeout);
62
+ },
63
+ async toBeFocused(options) {
64
+ await callExpect('toBeFocused', [], options?.timeout);
65
+ },
66
+ async toBeEmpty(options) {
67
+ await callExpect('toBeEmpty', [], options?.timeout);
68
+ },
69
+ async toBeInViewport(options) {
70
+ const ratio = options?.ratio;
71
+ await callExpect(
72
+ 'toBeInViewport',
73
+ ratio === undefined ? [] : [ratio],
74
+ options?.timeout,
75
+ );
76
+ },
77
+ async toHaveText(text, options) {
78
+ await callExpect(
79
+ 'toHaveText',
80
+ [serializeMatcherText(text)],
81
+ options?.timeout,
82
+ );
83
+ },
84
+ async toContainText(text, options) {
85
+ await callExpect(
86
+ 'toContainText',
87
+ [serializeMatcherText(text)],
88
+ options?.timeout,
89
+ );
90
+ },
91
+ async toHaveValue(value, options) {
92
+ await callExpect(
93
+ 'toHaveValue',
94
+ [serializeMatcherText(value)],
95
+ options?.timeout,
96
+ );
97
+ },
98
+ async toHaveId(value, options) {
99
+ await callExpect(
100
+ 'toHaveId',
101
+ [serializeMatcherText(value)],
102
+ options?.timeout,
103
+ );
104
+ },
105
+ async toHaveAttribute(name, value, options) {
106
+ const args =
107
+ value === undefined ? [name] : [name, serializeMatcherText(value)];
108
+ await callExpect('toHaveAttribute', args, options?.timeout);
109
+ },
110
+ async toHaveClass(value, options) {
111
+ await callExpect(
112
+ 'toHaveClass',
113
+ [serializeMatcherText(value)],
114
+ options?.timeout,
115
+ );
116
+ },
117
+ async toHaveCount(count, options) {
118
+ await callExpect('toHaveCount', [count], options?.timeout);
119
+ },
120
+ async toHaveCSS(name, value, options) {
121
+ if (typeof name !== 'string' || !name) {
122
+ throw new TypeError('toHaveCSS expects a non-empty CSS property name');
123
+ }
124
+ await callExpect(
125
+ 'toHaveCSS',
126
+ [name, serializeMatcherText(value)],
127
+ options?.timeout,
128
+ );
129
+ },
130
+ async toHaveJSProperty(name, value, options) {
131
+ if (typeof name !== 'string' || !name) {
132
+ throw new TypeError(
133
+ 'toHaveJSProperty expects a non-empty property name',
134
+ );
135
+ }
136
+ await callExpect('toHaveJSProperty', [name, value], options?.timeout);
137
+ },
138
+ };
139
+
140
+ const withNot = api as BrowserElementExpect;
141
+ Object.defineProperty(withNot, 'not', {
142
+ configurable: false,
143
+ enumerable: false,
144
+ get() {
145
+ return createElementExpect(locator, !isNot);
146
+ },
147
+ });
148
+ return withNot;
149
+ };
150
+
151
+ const element = (locator: unknown): BrowserElementExpect => {
152
+ if (!isLocator(locator)) {
153
+ throw new TypeError(
154
+ 'expect.element() expects a Locator returned from @rstest/browser page.getBy* APIs.',
155
+ );
156
+ }
157
+
158
+ return createElementExpect(locator, false);
159
+ };
160
+
161
+ const markBrowserElement = (): void => {
162
+ Object.defineProperty(element, '__rstestBrowser', {
163
+ value: true,
164
+ configurable: false,
165
+ enumerable: false,
166
+ writable: false,
167
+ });
168
+ };
169
+
170
+ const installExpectElement = (): void => {
171
+ // In browser runtime, `@rstest/core` exports are proxies that forward property
172
+ // access to `globalThis.RSTEST_API`. Patch the underlying expect implementation.
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ const api = (globalThis as any).RSTEST_API as any;
175
+ const target = api?.expect;
176
+ if (!target) {
177
+ throw new Error(
178
+ 'RSTEST_API.expect is not registered yet. This usually indicates @rstest/browser was imported too early.',
179
+ );
180
+ }
181
+
182
+ if (typeof target.element !== 'function' || !target.element.__rstestBrowser) {
183
+ markBrowserElement();
184
+ target.element = element;
185
+ }
186
+ };
187
+
188
+ installExpectElement();
189
+
190
+ export type {
191
+ BrowserPage,
192
+ BrowserSerializable,
193
+ LocatorBlurOptions,
194
+ LocatorCheckOptions,
195
+ LocatorClickOptions,
196
+ LocatorDblclickOptions,
197
+ LocatorDispatchEventInit,
198
+ LocatorFillOptions,
199
+ LocatorFilterOptions,
200
+ LocatorFocusOptions,
201
+ LocatorGetByRoleOptions,
202
+ LocatorHoverOptions,
203
+ LocatorKeyboardModifier,
204
+ LocatorMouseButton,
205
+ LocatorPosition,
206
+ LocatorPressOptions,
207
+ LocatorScrollIntoViewIfNeededOptions,
208
+ LocatorSelectOptionOptions,
209
+ LocatorSetInputFilesOptions,
210
+ LocatorTextOptions,
211
+ LocatorWaitForOptions,
212
+ } from './locator';
213
+ export { Locator, page, setTestIdAttribute };
@@ -0,0 +1,86 @@
1
+ import type { BrowserDispatchRequest } from '../protocol';
2
+ import { DISPATCH_METHOD_RPC, DISPATCH_NAMESPACE_BROWSER } from '../protocol';
3
+ import type { BrowserRpcRequest } from '../rpcProtocol';
4
+ import {
5
+ createRequestId,
6
+ dispatchRpc,
7
+ getRpcTimeout,
8
+ } from './dispatchTransport';
9
+
10
+ const getUrlSearchParam = (name: string): string | undefined => {
11
+ try {
12
+ const value = new URL(window.location.href).searchParams.get(name);
13
+ return value ?? undefined;
14
+ } catch {
15
+ return undefined;
16
+ }
17
+ };
18
+
19
+ const getCurrentTestPath = (): string => {
20
+ const testPath =
21
+ window.__RSTEST_BROWSER_OPTIONS__?.testFile ??
22
+ getUrlSearchParam('testFile');
23
+ if (!testPath) {
24
+ throw new Error(
25
+ 'Browser RPC requires testFile in __RSTEST_BROWSER_OPTIONS__. ' +
26
+ 'This usually indicates the runner iframe was not configured by the container or URL.',
27
+ );
28
+ }
29
+ return testPath;
30
+ };
31
+
32
+ const getCurrentRunId = (): string => {
33
+ const runId =
34
+ window.__RSTEST_BROWSER_OPTIONS__?.runId ?? getUrlSearchParam('runId');
35
+ if (!runId) {
36
+ throw new Error(
37
+ 'Browser RPC requires runId in __RSTEST_BROWSER_OPTIONS__. ' +
38
+ 'This usually indicates the runner iframe URL/config is stale or incomplete.',
39
+ );
40
+ }
41
+ return runId;
42
+ };
43
+
44
+ const createBrowserDispatchRequest = (
45
+ requestId: string,
46
+ request: BrowserRpcRequest,
47
+ ): BrowserDispatchRequest => {
48
+ return {
49
+ requestId,
50
+ namespace: DISPATCH_NAMESPACE_BROWSER,
51
+ method: DISPATCH_METHOD_RPC,
52
+ args: request,
53
+ target: {
54
+ testFile: request.testPath,
55
+ },
56
+ };
57
+ };
58
+
59
+ export const callBrowserRpc = async <T>(
60
+ payload: Omit<BrowserRpcRequest, 'id' | 'testPath' | 'runId'>,
61
+ ): Promise<T> => {
62
+ if (
63
+ payload.kind === 'config' &&
64
+ window.__RSTEST_BROWSER_OPTIONS__?.mode === 'collect'
65
+ ) {
66
+ return undefined as T;
67
+ }
68
+
69
+ const id = createRequestId('browser-rpc');
70
+ const rpcTimeout = getRpcTimeout();
71
+ const request: BrowserRpcRequest = {
72
+ id,
73
+ testPath: getCurrentTestPath(),
74
+ runId: getCurrentRunId(),
75
+ ...payload,
76
+ };
77
+ const dispatchRequest = createBrowserDispatchRequest(id, request);
78
+
79
+ return dispatchRpc<T>({
80
+ requestId: id,
81
+ request: dispatchRequest,
82
+ timeoutMs: rpcTimeout,
83
+ staleMessage: 'Stale browser RPC request ignored.',
84
+ timeoutMessage: `Browser RPC timeout after ${rpcTimeout / 1000}s: ${request.kind}.${request.method}`,
85
+ });
86
+ };