@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.
- package/LICENSE-APACHE-2.0 +202 -0
- package/NOTICE +11 -0
- package/dist/361.js +8 -0
- package/dist/augmentExpect.d.ts +73 -0
- package/dist/browser-container/container-static/css/index.5c72297783.css +1 -0
- package/dist/browser-container/container-static/js/{565.226c9ef5.js → 101.36a8ccdf84.js} +4024 -3856
- package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.c1d17467.js → index.28d833de0b.js} +732 -675
- package/dist/browser-container/container-static/js/{lib-react.97ee79b0.js → lib-react.dcf2a5e57a.js} +10 -10
- package/dist/browser-container/container-static/js/lib-react.dcf2a5e57a.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +583 -0
- package/dist/browserRpcRegistry.d.ts +18 -0
- package/dist/client/api.d.ts +3 -0
- package/dist/client/browserRpc.d.ts +2 -0
- package/dist/client/dispatchTransport.d.ts +11 -0
- package/dist/client/entry.d.ts +1 -5
- package/dist/client/locator.d.ts +125 -0
- package/dist/client/snapshot.d.ts +0 -6
- package/dist/concurrency.d.ts +12 -0
- package/dist/dispatchCapabilities.d.ts +34 -0
- package/dist/dispatchRouter.d.ts +20 -0
- package/dist/headedSerialTaskQueue.d.ts +8 -0
- package/dist/headlessLatestRerunScheduler.d.ts +19 -0
- package/dist/headlessTransport.d.ts +12 -0
- package/dist/hostController.d.ts +16 -0
- package/dist/index.js +1790 -296
- package/dist/protocol.d.ts +44 -33
- package/dist/providers/index.d.ts +79 -0
- package/dist/providers/playwright/compileLocator.d.ts +3 -0
- package/dist/providers/playwright/dispatchBrowserRpc.d.ts +13 -0
- package/dist/providers/playwright/expectUtils.d.ts +24 -0
- package/dist/providers/playwright/implementation.d.ts +2 -0
- package/dist/providers/playwright/index.d.ts +1 -0
- package/dist/providers/playwright/runtime.d.ts +5 -0
- package/dist/providers/playwright/textMatcher.d.ts +8 -0
- package/dist/rpcProtocol.d.ts +145 -0
- package/dist/runSession.d.ts +33 -0
- package/dist/sessionRegistry.d.ts +34 -0
- package/dist/sourceMap/sourceMapLoader.d.ts +14 -0
- package/dist/watchCliShortcuts.d.ts +6 -0
- package/dist/watchRerunPlanner.d.ts +21 -0
- package/package.json +17 -12
- package/src/AGENTS.md +128 -0
- package/src/augmentExpect.ts +62 -0
- package/src/browser.ts +3 -0
- package/src/browserRpcRegistry.ts +57 -0
- package/src/client/AGENTS.md +82 -0
- package/src/client/api.ts +213 -0
- package/src/client/browserRpc.ts +86 -0
- package/src/client/dispatchTransport.ts +178 -0
- package/src/client/entry.ts +96 -33
- package/src/client/locator.ts +452 -0
- package/src/client/snapshot.ts +32 -97
- package/src/client/sourceMapSupport.ts +26 -37
- package/src/concurrency.ts +62 -0
- package/src/dispatchCapabilities.ts +162 -0
- package/src/dispatchRouter.ts +82 -0
- package/src/env.d.ts +8 -1
- package/src/headedSerialTaskQueue.ts +19 -0
- package/src/headlessLatestRerunScheduler.ts +76 -0
- package/src/headlessTransport.ts +28 -0
- package/src/hostController.ts +1538 -384
- package/src/protocol.ts +66 -31
- package/src/providers/index.ts +103 -0
- package/src/providers/playwright/compileLocator.ts +130 -0
- package/src/providers/playwright/dispatchBrowserRpc.ts +372 -0
- package/src/providers/playwright/expectUtils.ts +57 -0
- package/src/providers/playwright/implementation.ts +33 -0
- package/src/providers/playwright/index.ts +1 -0
- package/src/providers/playwright/runtime.ts +32 -0
- package/src/providers/playwright/textMatcher.ts +10 -0
- package/src/rpcProtocol.ts +220 -0
- package/src/runSession.ts +110 -0
- package/src/sessionRegistry.ts +89 -0
- package/src/sourceMap/sourceMapLoader.ts +96 -0
- package/src/watchCliShortcuts.ts +77 -0
- package/src/watchRerunPlanner.ts +77 -0
- package/dist/browser-container/container-static/css/index.5a71c757.css +0 -1
- package/dist/browser-container/container-static/js/565.226c9ef5.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.97ee79b0.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/scheduler.5accca0c.js +0 -407
- 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,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
|
+
};
|