juce-webview-agent-bridge 0.3.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mateusz Siwiński
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # juce_webview_agent_bridge
2
+
3
+ [![tests](https://github.com/mateusz28011/juce-webview-agent-bridge/actions/workflows/tests.yml/badge.svg)](https://github.com/mateusz28011/juce-webview-agent-bridge/actions/workflows/tests.yml)
4
+
5
+ A **debug-only** JUCE module that lets an external agent (or script) drive a live
6
+ `juce::WebBrowserComponent` over a loopback TCP socket — a "mini-CDP" giving you a
7
+ browser-like toolkit on the **real** embedded WebView:
8
+
9
+ - **eval** arbitrary JavaScript and read the result;
10
+ - **stream** `console.*`, uncaught errors, and network — `fetch`/`XHR`,
11
+ `WebSocket` / `EventSource` (SSE) frames, `navigator.sendBeacon`, plus
12
+ `PerformanceObserver` resource timing;
13
+ - **DOM / click / fill** (React-safe synthetic input);
14
+ - **screenshot** the host window — or a cropped UI region — via the OS compositor
15
+ (captures WebGL/canvas, which in-WebView snapshot APIs can miss) on **macOS and
16
+ Windows 11**; Linux capture is a TODO.
17
+
18
+ No Chrome DevTools Protocol is required. **WKWebView** (macOS) exposes no CDP at all
19
+ for an embedded view; **WebView2** (Windows) can expose CDP, but only behind a
20
+ remote-debugging flag you don't want in a shipping build. This bridge gives you one
21
+ path that works the same on both without depending on that flag —
22
+ eval/console/network/DOM ride on `evaluateJavascript`, which every JUCE backend
23
+ already provides. macOS is the daily-use platform; Windows/WebView2 has also been
24
+ verified against the home project's real Debug standalone. See **Status** below.
25
+
26
+ > ⚠️ **Security.** The bridge evaluates arbitrary JS. Access is gated only by the
27
+ > loopback (`127.0.0.1`) bind plus a per-session token — and the token sits in
28
+ > plaintext in the discovery file, so **loopback is the real trust boundary** (there
29
+ > is no TLS; anything that can reach localhost + read your home dir is trusted). It is
30
+ > gated to `JUCE_DEBUG` by default (`WEB_AGENT_BRIDGE_ENABLED`). **Never ship it
31
+ > enabled in a release build.**
32
+
33
+ ## Why
34
+
35
+ This started in *Better Message Mycelia*, a JUCE app with a dense React +
36
+ PixiJS/WebGL plugin UI. A browser copy could not reproduce native state or timing,
37
+ WKWebView had no CDP, and in-page screenshots missed GPU content. The bridge made
38
+ the running plugin observable and controllable, which enabled:
39
+
40
+ - agent debugging against the real UI and C++ engine;
41
+ - live e2e tests for controls, presets and native state;
42
+ - performance probes using event streams and rAF frame-gap measurements.
43
+
44
+ ### Why not just Playwright?
45
+
46
+ Playwright cannot attach to WKWebView and needs an explicitly enabled CDP endpoint
47
+ for WebView2. This bridge instead uses JUCE's existing `evaluateJavascript` and
48
+ native-function surfaces. It can also capture compositor-rendered WebGL and assert
49
+ on real C++ state through `backend()`.
50
+
51
+ It is not a Playwright replacement: input is synthetic, network is observe-only,
52
+ and there is one page. You can still use the `@playwright/test` runner as a harness;
53
+ see [`examples/playwright-test`](examples/playwright-test).
54
+
55
+ ## Status — what's actually been exercised
56
+
57
+ Honest test coverage, so you know what you're getting:
58
+
59
+ | Area | macOS (WKWebView) | Windows (WebView2) | Linux |
60
+ |---|---|---|---|
61
+ | eval / console / network / DOM / e2e | ✅ used daily against a real plugin | ✅ verified against a real WebView2 Debug standalone | ⚠️ untested against a real app |
62
+ | Native screenshot (`shot`) | ✅ (macOS 14+, ScreenCaptureKit) | ✅ (Windows 11, Windows.Graphics.Capture + D3D11) | ❌ TODO |
63
+ | C++ + JS test suites | ✅ CI | ✅ CI | ✅ CI |
64
+
65
+ The Windows real-app verification covers discovery/auth, Unicode eval, console and
66
+ fetch capture, DOM locators, React-safe fill/click, accessibility snapshots, large
67
+ chunked reads, sink replay, and native full-window/region screenshots. WebKit layer
68
+ inspection remains macOS-only. Linux real-app reports and fixes are very welcome.
69
+
70
+ ## Install
71
+
72
+ **Requirements:** JUCE 8, C++17, a `juce::WebBrowserComponent`-based UI; the
73
+ clients need Node ≥ 18 (zero runtime dependencies).
74
+
75
+ It's a standard JUCE module. Get the repo — as a submodule:
76
+
77
+ ```bash
78
+ git submodule add https://github.com/mateusz28011/juce-webview-agent-bridge.git modules/juce-webview-agent-bridge
79
+ ```
80
+
81
+ or with CMake `FetchContent` (after JUCE has been added, so
82
+ `juce_add_module` is available):
83
+
84
+ ```cmake
85
+ include(FetchContent)
86
+ FetchContent_Declare(juce_webview_agent_bridge
87
+ GIT_REPOSITORY https://github.com/mateusz28011/juce-webview-agent-bridge.git
88
+ GIT_TAG v0.3.0)
89
+ FetchContent_MakeAvailable(juce_webview_agent_bridge)
90
+ ```
91
+
92
+ The module itself lives in the `juce_webview_agent_bridge/` subdirectory (JUCE requires the
93
+ module directory to be named exactly like the module ID). Register and link it:
94
+
95
+ ```cmake
96
+ # Submodule/manual checkout:
97
+ juce_add_module(path/to/juce-webview-agent-bridge/juce_webview_agent_bridge)
98
+
99
+ # FetchContent_MakeAvailable registers the module through the repo's root CMakeLists.
100
+ target_link_libraries(MyPlugin PRIVATE juce_webview_agent_bridge)
101
+ ```
102
+
103
+ Install the zero-runtime-dependency Node client separately in the project that
104
+ owns your e2e tests. The npm package is independent of how the C++ module was
105
+ fetched and includes strict TypeScript declarations:
106
+
107
+ ```bash
108
+ npm install --save-dev juce-webview-agent-bridge
109
+ npx juce-webview-agent-bridge ping
110
+ ```
111
+
112
+ ## Integrate (3 steps)
113
+
114
+ ```cpp
115
+ #include <juce_webview_agent_bridge/juce_webview_agent_bridge.h>
116
+
117
+ // 1. Fold capture into the WebView Options (before creating the WebView):
118
+ auto bridge = std::make_shared<web_agent::WebAgentBridge>();
119
+ options = web_agent::withCapture (std::move (options), bridge); // no-op if disabled
120
+
121
+ // 2. After the WebView exists, wire eval + bounds and start listening:
122
+ web_agent::connect (*bridge, *webView, *this /* component for screen bounds */);
123
+ bridge->start(); // 127.0.0.1:8930
124
+
125
+ // 3. On teardown, before the WebView is destroyed:
126
+ bridge->stop();
127
+ ```
128
+
129
+ Wrap the calls in `#if WEB_AGENT_BRIDGE_ENABLED` if you keep the `bridge` member
130
+ unconditionally (a `std::shared_ptr` to a forward-declared type is fine).
131
+
132
+ ## Agent skill
133
+
134
+ `skills/juce-webview-agent-bridge/` is a [skill](https://skills.sh) that teaches a coding agent
135
+ (e.g. Claude Code) to drive the bridge — commands, the e2e client, diagnostic
136
+ techniques, and the gotchas below in agent-digestible form:
137
+
138
+ ```bash
139
+ npx skills add mateusz28011/juce-webview-agent-bridge
140
+ ```
141
+
142
+ ## CLI client
143
+
144
+ With the npm package installed, use `npx juce-webview-agent-bridge`. A checkout can run the committed
145
+ `tools/web-agent.mjs` build directly without installing dependencies:
146
+
147
+ ```bash
148
+ npx juce-webview-agent-bridge ping
149
+ npx juce-webview-agent-bridge eval "document.title"
150
+ npx juce-webview-agent-bridge logs # live console + network stream
151
+ npx juce-webview-agent-bridge shot /tmp/ui.png # native compositor shot of the host window
152
+ npx juce-webview-agent-bridge shot /tmp/panel.png "#panel" # crop to an element
153
+ ```
154
+
155
+ Other commands: `hello`, `dom`, `click`, `fill`, `capture`, `backlog`,
156
+ `layerdebug`, and `layertree`. Port and token are normally auto-discovered.
157
+
158
+ ## E2E (Playwright-style)
159
+
160
+ The package root exports a Playwright-shaped client with client-side auto-waiting:
161
+
162
+ ```js
163
+ import { connect, expect } from 'juce-webview-agent-bridge';
164
+
165
+ const page = await connect({ activate: 'My App' });
166
+ await page.getByTestId('patch-name').fill('warm pad', { enter: true });
167
+ await page.locator('text=Save').click();
168
+ await expect(page.locator('.patch-row')).toHaveCount(1);
169
+ await expect.poll(() => page.backend('getPatchCount')).toBe(1);
170
+ page.close();
171
+ ```
172
+
173
+ The client includes CSS/text/role locators, click/fill/type/drag actions,
174
+ auto-retrying assertions, accessibility snapshots, native screenshots, live
175
+ console/network/error events, large-value reads, render-performance probes and
176
+ JUCE native calls. Full API and operational guidance: [docs/e2e.md](docs/e2e.md).
177
+
178
+ ## Discovery & auth
179
+
180
+ On `start()` the host binds `127.0.0.1:8930` (scanning upward on collision) and
181
+ publishes `{port, token}` to `~/.web_agent_bridge.json` (plus a per-instance file
182
+ under `~/.web_agent_bridge.d/` so several hosts coexist). Clients read it
183
+ automatically; a random per-session **token** gates every connection. The wire is
184
+ newline-delimited JSON over TCP (`hello` / `ping` / `auth` / `eval` / `bounds` /
185
+ `shot` / `layerdebug` / `layertree` / `sink_replay` + the unsolicited `sink` event stream) — the full op table,
186
+ sink-frame format, and discovery details are in [docs/protocol.md](docs/protocol.md).
187
+
188
+ ## Known limits
189
+
190
+ - Native screenshots require macOS 14+ or Windows 11 and a compositor-capturable,
191
+ non-minimized window. macOS permission/signing setup is in
192
+ [docs/screen-recording.md](docs/screen-recording.md). Linux capture is a TODO.
193
+ - **Synthetic input** dispatched from JS has `isTrusted === false`, so APIs gated
194
+ behind a user gesture (file pickers, some clipboard/fullscreen) are out of reach.
195
+ - **eval errors** are only reported on WKWebView; on WebView2 a failed eval looks
196
+ like a `null` result — rely on the console/error stream there.
197
+
198
+ ## Testing
199
+
200
+ Both suites live in `tests/` and run **independently of any host app**, so they
201
+ keep working after this module is extracted into its own repo.
202
+
203
+ ```bash
204
+ # JS client — zero dependencies, Node >= 18
205
+ npm test # or: node --test tests/*.test.mjs
206
+
207
+ # Maintainer build — strict TypeScript source -> committed .mjs + .d.mts
208
+ npm ci --ignore-scripts
209
+ npm run build
210
+ npm run test:types
211
+
212
+ # C++ bridge — fetches JUCE + Catch2 on first configure
213
+ cmake -S tests -B build/test
214
+ cmake --build build/test
215
+ ctest --test-dir build/test --output-on-failure
216
+
217
+ # reuse a local JUCE checkout instead of downloading it:
218
+ cmake -S tests -B build/test -DWAB_JUCE_DIR=/path/to/JUCE
219
+ ```
220
+
221
+ The JS tests drive the real clients against a mock bridge; the C++ suite drives
222
+ the real loopback server. Neither requires a host app.
223
+
224
+ ## Contributing
225
+
226
+ See [CONTRIBUTING.md](CONTRIBUTING.md) — ground rules (app-agnostic, zero client
227
+ deps, debug-only), how to run both test suites, and the release flow. Linux capture
228
+ support and Linux real-app verification are the most wanted contributions.
229
+
230
+ ## License
231
+
232
+ MIT — see `LICENSE`.
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "juce-webview-agent-bridge",
3
+ "version": "0.3.0",
4
+ "description": "CLI + protocol client for the juce_webview_agent_bridge debug WebView bridge (eval/console/network/click/fill/screenshot over a loopback socket, no CDP).",
5
+ "type": "module",
6
+ "types": "./tools/e2e.d.mts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./tools/e2e.d.mts",
10
+ "import": "./tools/e2e.mjs"
11
+ },
12
+ "./e2e": {
13
+ "types": "./tools/e2e.d.mts",
14
+ "import": "./tools/e2e.mjs"
15
+ },
16
+ "./shared": {
17
+ "types": "./tools/shared.d.mts",
18
+ "import": "./tools/shared.mjs"
19
+ }
20
+ },
21
+ "bin": {
22
+ "juce-webview-agent-bridge": "tools/web-agent.mjs"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "test": "node --test tests/*.test.mjs",
27
+ "test:types": "tsc -p tests/types/tsconfig.json",
28
+ "prepublishOnly": "npm run build && npm test && npm run test:types",
29
+ "release": "bash scripts/release.sh"
30
+ },
31
+ "files": [
32
+ "tools/",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/mateusz28011/juce-webview-agent-bridge.git"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^24.0.0",
49
+ "typescript": "^5.9.3"
50
+ }
51
+ }
@@ -0,0 +1,359 @@
1
+ import type { Socket } from 'node:net';
2
+ type ProtocolMessage = Record<string, any>;
3
+ type LogFn = (message: string) => void;
4
+ type TimeoutOptions = {
5
+ timeout?: number;
6
+ };
7
+ type RequestOptions = {
8
+ timeoutMs?: number;
9
+ };
10
+ type PollOptions = TimeoutOptions & {
11
+ interval?: number;
12
+ };
13
+ export type LayerBounds = {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ };
19
+ export type ClipRect = {
20
+ x: number;
21
+ y: number;
22
+ w: number;
23
+ h: number;
24
+ };
25
+ export type AriaNode = {
26
+ role: string;
27
+ name?: string;
28
+ value?: string;
29
+ checked?: boolean;
30
+ disabled?: boolean;
31
+ children?: AriaNode[];
32
+ };
33
+ export type SinkEvent<T = unknown> = {
34
+ kind: string;
35
+ t: number;
36
+ data: T;
37
+ seq?: number;
38
+ };
39
+ export type NetworkEventData = {
40
+ kind?: string;
41
+ url?: string;
42
+ method?: string;
43
+ status?: number;
44
+ [key: string]: unknown;
45
+ };
46
+ export type Capabilities = {
47
+ protocolVersion: number;
48
+ platform: string;
49
+ ops: string[];
50
+ screenshotAvailable: boolean;
51
+ authRequired: boolean;
52
+ };
53
+ export type RenderPerfResult = {
54
+ durMs: number;
55
+ commitsPerSec: number;
56
+ p50gap: number;
57
+ p95gap: number;
58
+ p99gap: number;
59
+ maxGap: number;
60
+ framesOver24: number;
61
+ framesOver50: number;
62
+ measuredHz: number;
63
+ frameBudgetMs: number;
64
+ framesDroppedRel: number;
65
+ framesDropped2x: number;
66
+ p99Frames: number;
67
+ motion: boolean;
68
+ };
69
+ export type ConnectOptions = {
70
+ host?: string;
71
+ port?: number;
72
+ token?: string;
73
+ timeout?: number;
74
+ interval?: number;
75
+ log?: LogFn;
76
+ logFile?: string;
77
+ logEcho?: boolean;
78
+ backendTimeoutMs?: number;
79
+ activate?: string;
80
+ };
81
+ export type LocatorState = {
82
+ visible: boolean;
83
+ enabled: boolean;
84
+ editable: boolean;
85
+ hit: boolean;
86
+ box: ClipRect;
87
+ text: string;
88
+ value: string | null;
89
+ checked: boolean;
90
+ };
91
+ export type LocatorProbe = {
92
+ n: number;
93
+ state: LocatorState | null;
94
+ };
95
+ export type ActionOptions = TimeoutOptions & {
96
+ force?: boolean;
97
+ };
98
+ export type FillOptions = TimeoutOptions & {
99
+ enter?: boolean;
100
+ };
101
+ export type DragOptions = TimeoutOptions & {
102
+ dx?: number;
103
+ dy?: number;
104
+ steps?: number;
105
+ settleMs?: number;
106
+ stepMs?: number;
107
+ pointer?: boolean;
108
+ };
109
+ export type WaitForOptions = TimeoutOptions & {
110
+ state?: 'attached' | 'detached' | 'visible' | 'hidden';
111
+ };
112
+ export type PollExpectOptions = PollOptions & {
113
+ message?: string;
114
+ };
115
+ /** Bring the host app's window to the foreground (macOS, best-effort; resolves
116
+ * false elsewhere). A backgrounded WebView reports document.hidden === true and
117
+ * many apps pause timers/polling/state-sync — an agent then reads stale or empty
118
+ * state even though eval works. Foregrounding the REAL window (not faking the
119
+ * visibility signal) reproduces the user-visible condition, so tests assert on
120
+ * live state. connect({ activate: '<App Name>' }) calls this for you. */
121
+ export declare function activateApp(appName?: string): Promise<boolean>;
122
+ /** A logger that appends timestamped action lines to ONE file and (by default)
123
+ * echoes them to stderr so a live run shows progress instead of going silent.
124
+ * Generic/agnostic: it records whatever the caller logs. connect() wires one up
125
+ * automatically (override via { log } or { logFile }, or $WAE_LOG_FILE). */
126
+ /** Parse a `layertree` op dump (the `_caLayerTreeAsText` format) into a flat
127
+ array of layer bounds `{ x, y, width, height }` — the programmatic
128
+ compositing-layer census. Pure; pairs with `page.layerTree()`:
129
+
130
+ const layers = parseLayerTree(await page.layerTree());
131
+ const canvases = layers.filter(l => l.width === 398 && l.height === 209);
132
+
133
+ The dump nests layers as parenthesized blocks; for a census the flat list
134
+ of `(layer bounds [x: … y: … width: … height: …])` entries is what counts,
135
+ so nesting is deliberately ignored. */
136
+ export declare function parseLayerTree(text: string): LayerBounds[];
137
+ export declare function fileLogger(file: string, { echo }?: {
138
+ echo?: boolean;
139
+ }): LogFn;
140
+ export declare const PAGE_HELPERS: string;
141
+ declare class Session {
142
+ readonly sock: Socket;
143
+ readonly token: string;
144
+ readonly pending: Map<number, {
145
+ resolve: (value: ProtocolMessage) => void;
146
+ reject: (error: unknown) => void;
147
+ }>;
148
+ private _id;
149
+ readonly sinkListeners: Set<(event: SinkEvent<any>) => void>;
150
+ constructor(sock: Socket, token: string);
151
+ onSink(fn: (event: SinkEvent<any>) => void): () => void;
152
+ _emitSink(event: SinkEvent<any>): void;
153
+ _failAll(err: unknown): void;
154
+ request(obj: ProtocolMessage, { timeoutMs }?: RequestOptions): Promise<ProtocolMessage>;
155
+ evalRaw<T = unknown>(code: string, opts?: RequestOptions): Promise<T>;
156
+ close(): void;
157
+ }
158
+ export declare function connect({ host, port, token, timeout, interval, log, logFile, logEcho, backendTimeoutMs, activate }?: ConnectOptions): Promise<Page>;
159
+ export declare class Page {
160
+ readonly session: Session;
161
+ readonly defaultTimeout: number;
162
+ readonly interval: number;
163
+ readonly backendTimeoutMs: number;
164
+ readonly log: LogFn;
165
+ logFile: string | null;
166
+ constructor(session: Session, { defaultTimeout, interval, log, backendTimeoutMs }: {
167
+ defaultTimeout: number;
168
+ interval: number;
169
+ log: LogFn;
170
+ backendTimeoutMs?: number;
171
+ });
172
+ locator(selector: string): Locator;
173
+ getByTestId(id: string): Locator;
174
+ /** Escape hatch: run arbitrary JS in the page and get the result (small results). */
175
+ evaluate<T = unknown>(code: string, opts?: RequestOptions): Promise<T>;
176
+ /** Read a string-valued JS expression in <=chunk slices. WKWebView's
177
+ evaluateJavascript stalls on large (>~100KB) returns, so big values are
178
+ pulled in pieces. `expr` must evaluate to (or stringify to) a string. */
179
+ readBig(expr: string, { chunk, timeoutMs }?: {
180
+ chunk?: number;
181
+ timeoutMs?: number;
182
+ }): Promise<string>;
183
+ /** Structured accessibility snapshot of the page (or a subtree) — a compact
184
+ role/name tree with value/checked/disabled, generic containers flattened away.
185
+ Token-cheap vs outerHTML. Read via readBig so a large tree doesn't stall. */
186
+ ariaSnapshot(): Promise<AriaNode | AriaNode[]>;
187
+ /** Invoke a JUCE native function (registered via withNativeFunction) by name and
188
+ await its result. Good for small/medium results. Very large results (>~100KB)
189
+ stall JUCE's C->JS completion delivery regardless of timeout — that ceiling is
190
+ WKWebView's, not the bridge's, so a longer wait will not help. For bulk state,
191
+ read the juce:// resource route instead (e.g. juce://juce.backend/sequencerState.json
192
+ or dialogueGroups.json), or stash the value in a page variable and pull it with
193
+ readBig(). Requires a JUCE WebView host. The completion-poll deadline is the
194
+ connect() `backendTimeoutMs` option (default 10s). */
195
+ backend<T = unknown>(name: string, ...params: unknown[]): Promise<T>;
196
+ /** Fire a JUCE native function without awaiting a result (resultId = -1). */
197
+ fireBackend(name: string, ...params: unknown[]): Promise<boolean>;
198
+ /** Subscribe to live page events. kind: 'console' | 'error' | 'net' | '*'.
199
+ The handler receives the raw sink event { kind, t, data }. Returns an
200
+ unsubscribe fn. (data shapes mirror the CLI `logs` output.) */
201
+ on<T = unknown>(kind: 'console' | 'error' | 'net' | '*', handler: (event: SinkEvent<T>) => void): () => void;
202
+ /** Resolve with the first sink event of `kind` (optionally matching predicate),
203
+ or reject on timeout. predicate receives the raw event { kind, t, data }. */
204
+ waitForEvent<T = unknown>(kind: 'console' | 'error' | 'net' | '*', predicate?: ((event: SinkEvent<T>) => boolean) | TimeoutOptions, { timeout }?: TimeoutOptions): Promise<SinkEvent<T>>;
205
+ /** Resolve with the network event `data` for the first fetch/XHR whose URL
206
+ contains `urlOrPredicate` (string) or for which predicate(data) is true.
207
+ Mirrors Playwright's page.waitForResponse over the observe-only net stream. */
208
+ waitForResponse<T extends NetworkEventData = NetworkEventData>(urlOrPredicate: string | ((data: T) => boolean), opts?: TimeoutOptions): Promise<T>;
209
+ /** Ask the host to re-send buffered sink events with seq > since (default 0) on
210
+ THIS socket — catch-up without the read-backlog / open-stream race. Replayed
211
+ events flow through the same on()/waitForEvent listeners (each carries a
212
+ monotonic `seq` for dedup). Resolves with the number of events replayed. */
213
+ replayEvents({ since }?: {
214
+ since?: number;
215
+ }): Promise<number>;
216
+ /** Poll a JS boolean expression in the page until it is truthy (or time out).
217
+ `expr` is evaluated as `!!(expr)`; exceptions count as not-yet-true. */
218
+ waitForFunction(expr: string, { timeout, interval }?: PollOptions): Promise<void>;
219
+ /** Evaluate a small JS expression repeatedly until pred(value) holds (or timeout).
220
+ Settle primitive — replaces fixed sleeps after an action. Unlike waitForFunction
221
+ it returns the LAST VALUE seen (never throws on timeout), so the caller asserts
222
+ on it and a timeout surfaces as a normal assertion failure with the real value. */
223
+ poll<T = unknown>(expr: string, pred: (value: T) => boolean, { timeout, interval }?: PollOptions): Promise<T>;
224
+ /** Read a small expression until it stops changing (`settles` equal reads in a row)
225
+ or timeout — for values that ramp over several frames (a knob drag updates via
226
+ rAF + a native round-trip), so assertions see the SETTLED value, not a mid-ramp
227
+ one. Returns the last value read. */
228
+ pollStable<T = unknown>(expr: string, { timeout, interval, settles }?: PollOptions & {
229
+ settles?: number;
230
+ }): Promise<T>;
231
+ /** Main-thread render-perf probe. Over durationMs, measures React commit rate
232
+ (via the DevTools onCommitFiberRoot hook, when present) and rAF frame-gap
233
+ percentiles — the UI thread any canvas/Pixi/WebGL animation shares, so a high
234
+ p99 gap IS a visible stutter. Gap thresholds are refresh-relative (median gap
235
+ = the monitor's frame period), so numbers are valid on 60/120/144Hz alike.
236
+ Pass { motionSelector } (a querySelectorAll selector) to also report `motion`:
237
+ whether any matched element's `d`/`transform`/`style` changed during the
238
+ window (e.g. "did the modulated knobs actually move"). */
239
+ measureRenderPerf({ durationMs, motionSelector }?: {
240
+ durationMs?: number;
241
+ motionSelector?: string | null;
242
+ }): Promise<RenderPerfResult>;
243
+ /** Capabilities handshake: { protocolVersion, platform, ops, screenshotAvailable,
244
+ authRequired }. Lets a caller branch on what the host supports (e.g. skip
245
+ screenshots when screenshotAvailable is false) without probing op-by-op. */
246
+ capabilities(): Promise<Capabilities>;
247
+ /** Toggle WebKit's compositing debug overlays (layer borders + repaint
248
+ counters) on the host's WKWebView via the bridge `layerdebug` op. The
249
+ overlays render into the window, so `screenshot()` captures them — count
250
+ layers / attribute repaints from a script, no Web Inspector session.
251
+ macOS-only; throws where the backend has no such SPI. Remember to turn it
252
+ OFF before any pixel-comparison capture: the overlays are pixels too. */
253
+ layerDebug(enabled?: boolean): Promise<true>;
254
+ /** Dump the WKWebView's remote CALayer tree as text via the bridge
255
+ `layertree` op — the programmatic counterpart of layerDebug(): parse it
256
+ to census compositing layers (count, geometry) from a script instead of
257
+ reading overlay pixels off a screenshot. macOS-only; throws elsewhere. */
258
+ layerTree(): Promise<string>;
259
+ /** Native screenshot of the host window (incl. WebGL) via the bridge `shot` op.
260
+ Writes a PNG host-side and returns its path. Pass { path } to choose where, and
261
+ { clip: {x,y,w,h} } (CSS px) to crop to a UI region for a much smaller PNG. */
262
+ screenshot({ path, clip }?: {
263
+ path?: string;
264
+ clip?: ClipRect;
265
+ }): Promise<string>;
266
+ close(): void;
267
+ }
268
+ export declare class Locator {
269
+ readonly page: Page;
270
+ readonly selector: string;
271
+ readonly index: number | null;
272
+ constructor(page: Page, selector: string, index?: number | null);
273
+ /** Narrow to the i-th match (0-based); negative or null = last match. */
274
+ nth(i: number): Locator;
275
+ first(): Locator;
276
+ _probe(opts?: RequestOptions): Promise<LocatorProbe>;
277
+ count(): Promise<number>;
278
+ isVisible(): Promise<boolean>;
279
+ textContent(): Promise<string | null>;
280
+ getAttribute(name: string): Promise<string | null>;
281
+ _waitStable({ needEnabled, force, timeout, what }: ActionOptions & {
282
+ needEnabled?: boolean;
283
+ what: string;
284
+ }): Promise<LocatorProbe>;
285
+ click({ timeout, force }?: ActionOptions): Promise<void>;
286
+ fill(value: string | number, { timeout, enter }?: FillOptions): Promise<void>;
287
+ /** Hover the element centre (pointerover/mouseover) — opens hover menus/tooltips.
288
+ Hover doesn't require enabled: tooltips on disabled controls are legitimate. */
289
+ hover({ timeout, force }?: ActionOptions): Promise<void>;
290
+ /** Double click (full single-click sequence twice + dblclick). */
291
+ dblclick({ timeout, force }?: ActionOptions): Promise<void>;
292
+ /** Type char-by-char (per-key events) — for inputs that react to keydown, not
293
+ just value changes. Use fill() for a one-shot set; type() for keystroke fidelity. */
294
+ type(value: string | number, { timeout }?: TimeoutOptions): Promise<void>;
295
+ /** Press a single key on the element (keydown+keyup), keeping focus. */
296
+ press(key: string, { timeout }?: TimeoutOptions): Promise<void>;
297
+ /** Select an <option> by value (then by visible label/text). */
298
+ selectOption(value: string, { timeout }?: TimeoutOptions): Promise<void>;
299
+ /** Ensure a checkbox/radio is checked (no-op if already). */
300
+ check({ timeout }?: TimeoutOptions): Promise<void>;
301
+ /** Ensure a checkbox is unchecked (no-op if already). */
302
+ uncheck({ timeout }?: TimeoutOptions): Promise<void>;
303
+ _setChecked(desired: boolean, timeout?: number): Promise<void>;
304
+ /** Focus the element. */
305
+ focus({ timeout }?: TimeoutOptions): Promise<void>;
306
+ /** Structured accessibility snapshot rooted at this element (see Page.ariaSnapshot). */
307
+ ariaSnapshot({ timeout }?: TimeoutOptions): Promise<AriaNode | AriaNode[] | null>;
308
+ /** Native screenshot cropped to this element's bounding box (a small PNG — only
309
+ the element's pixels, so far cheaper to read back than a full-window shot). */
310
+ screenshot({ path, timeout }?: TimeoutOptions & {
311
+ path?: string;
312
+ }): Promise<string>;
313
+ /** Press at the element's centre and drag by (dx, dy) px (default vertical, for
314
+ knobs/sliders). steps interpolates the move; settleMs lets the component
315
+ attach its document move/up listeners (a React effect) after mousedown. */
316
+ drag({ dx, dy, steps, settleMs, stepMs, pointer, timeout }?: DragOptions): Promise<void>;
317
+ waitFor({ state, timeout }?: WaitForOptions): Promise<void>;
318
+ _waitUntil(pred: (probe: LocatorProbe) => boolean, timeout: number | undefined, what: string): Promise<LocatorProbe>;
319
+ }
320
+ export interface LocatorAssertionMatchers {
321
+ toBeVisible(options?: TimeoutOptions): Promise<LocatorProbe>;
322
+ toBeHidden(options?: TimeoutOptions): Promise<LocatorProbe>;
323
+ toBeEnabled(options?: TimeoutOptions): Promise<LocatorProbe>;
324
+ toBeDisabled(options?: TimeoutOptions): Promise<LocatorProbe>;
325
+ toBeChecked(options?: TimeoutOptions): Promise<LocatorProbe>;
326
+ toHaveCount(count: number, options?: TimeoutOptions): Promise<LocatorProbe>;
327
+ toHaveText(expected: string | RegExp, options?: TimeoutOptions): Promise<LocatorProbe>;
328
+ toContainText(expected: string, options?: TimeoutOptions): Promise<LocatorProbe>;
329
+ toHaveValue(expected: string | RegExp, options?: TimeoutOptions): Promise<LocatorProbe>;
330
+ }
331
+ export interface LocatorAssertions extends LocatorAssertionMatchers {
332
+ readonly not: LocatorAssertionMatchers;
333
+ }
334
+ /** Value-level polling assertion (escape hatch for app state, e.g. over backend()):
335
+ await expect.poll(() => page.backend('getBpm')).toBe(128);
336
+ await expect.poll(async () => (await page.backend('getRows')).length).toBeGreaterThan(0);
337
+ `fn` is re-invoked every `interval` ms until the matcher holds or `timeout` ms
338
+ elapse; a throwing `fn` counts as "not yet". `.not` inverts any matcher. */
339
+ export interface PollAssertionMatchers<T> {
340
+ toBe(expected: T): Promise<T>;
341
+ toEqual(expected: T): Promise<T>;
342
+ toBeTruthy(): Promise<T>;
343
+ toBeFalsy(): Promise<T>;
344
+ toContain(expected: unknown): Promise<T>;
345
+ toBeGreaterThan(expected: number): Promise<T>;
346
+ toBeGreaterThanOrEqual(expected: number): Promise<T>;
347
+ toBeLessThan(expected: number): Promise<T>;
348
+ toBeLessThanOrEqual(expected: number): Promise<T>;
349
+ toSatisfy(predicate: (value: T) => boolean): Promise<T>;
350
+ }
351
+ export interface PollAssertions<T> extends PollAssertionMatchers<T> {
352
+ readonly not: PollAssertionMatchers<T>;
353
+ }
354
+ export interface ExpectFunction {
355
+ (locator: Locator): LocatorAssertions;
356
+ poll<T>(fn: () => T | Promise<T>, options?: PollExpectOptions): PollAssertions<T>;
357
+ }
358
+ export declare const expect: ExpectFunction;
359
+ export {};