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 +21 -0
- package/README.md +232 -0
- package/package.json +51 -0
- package/tools/e2e.d.mts +359 -0
- package/tools/e2e.mjs +998 -0
- package/tools/shared.d.mts +21 -0
- package/tools/shared.mjs +76 -0
- package/tools/web-agent.d.mts +2 -0
- package/tools/web-agent.mjs +251 -0
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
|
+
[](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
|
+
}
|
package/tools/e2e.d.mts
ADDED
|
@@ -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 {};
|