@petukhovart/agent-view 0.13.1 → 0.15.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +21 -21
- package/LICENSE +21 -21
- package/README.md +82 -3
- package/dist/cdp/transport.d.ts.map +1 -1
- package/dist/cdp/transport.js +154 -15
- package/dist/cdp/transport.js.map +1 -1
- package/dist/cdp/types.d.ts +56 -0
- package/dist/cdp/types.d.ts.map +1 -1
- package/dist/cdp/types.js.map +1 -1
- package/dist/cli/commands/click.d.ts +1 -0
- package/dist/cli/commands/click.d.ts.map +1 -1
- package/dist/cli/commands/click.js +2 -0
- package/dist/cli/commands/click.js.map +1 -1
- package/dist/cli/commands/coverage.d.ts +14 -0
- package/dist/cli/commands/coverage.d.ts.map +1 -0
- package/dist/cli/commands/coverage.js +34 -0
- package/dist/cli/commands/coverage.js.map +1 -0
- package/dist/cli/commands/heap.d.ts +14 -0
- package/dist/cli/commands/heap.d.ts.map +1 -0
- package/dist/cli/commands/heap.js +25 -0
- package/dist/cli/commands/heap.js.map +1 -0
- package/dist/cli/commands/listeners.d.ts +11 -0
- package/dist/cli/commands/listeners.d.ts.map +1 -0
- package/dist/cli/commands/listeners.js +34 -0
- package/dist/cli/commands/listeners.js.map +1 -0
- package/dist/cli/index.js +92 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/inspectors/coverage/index.d.ts +17 -0
- package/dist/inspectors/coverage/index.d.ts.map +1 -0
- package/dist/inspectors/coverage/index.js +62 -0
- package/dist/inspectors/coverage/index.js.map +1 -0
- package/dist/inspectors/format.d.ts +6 -0
- package/dist/inspectors/format.d.ts.map +1 -0
- package/dist/inspectors/format.js +17 -0
- package/dist/inspectors/format.js.map +1 -0
- package/dist/inspectors/heap/index.d.ts +65 -0
- package/dist/inspectors/heap/index.d.ts.map +1 -0
- package/dist/inspectors/heap/index.js +269 -0
- package/dist/inspectors/heap/index.js.map +1 -0
- package/dist/inspectors/listeners/index.d.ts +4 -0
- package/dist/inspectors/listeners/index.d.ts.map +1 -0
- package/dist/inspectors/listeners/index.js +30 -0
- package/dist/inspectors/listeners/index.js.map +1 -0
- package/dist/server/server.d.ts +21 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +150 -9
- package/dist/server/server.js.map +1 -1
- package/package.json +2 -1
- package/skills/verify/SKILL.md +163 -427
- package/skills/verify/references/commands.md +384 -0
- package/skills/verify/references/design-conformance.md +23 -0
- package/skills/verify/references/memory-leaks.md +94 -0
- package/dist/cdp/console-stream.d.ts +0 -35
- package/dist/cdp/console-stream.d.ts.map +0 -1
- package/dist/cdp/console-stream.js +0 -103
- package/dist/cdp/console-stream.js.map +0 -1
- package/dist/inspectors/dom.d.ts +0 -17
- package/dist/inspectors/dom.d.ts.map +0 -1
- package/dist/inspectors/dom.js +0 -131
- package/dist/inspectors/dom.js.map +0 -1
- package/dist/inspectors/scene/pixi.d.ts +0 -3
- package/dist/inspectors/scene/pixi.d.ts.map +0 -1
- package/dist/inspectors/scene/pixi.js +0 -52
- package/dist/inspectors/scene/pixi.js.map +0 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "agent-view",
|
|
10
10
|
"source": "./",
|
|
11
|
-
"description": "Visual verification CLI for
|
|
11
|
+
"description": "Visual verification CLI for Electron/Tauri/Chromium apps via Chrome DevTools Protocol. Ships with verify skill for easy and cheap checks against a live app, including heap-snapshot memory-leak debugging."
|
|
12
12
|
}
|
|
13
13
|
]
|
|
14
14
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "agent-view",
|
|
3
|
-
"description": "Visual verification CLI for
|
|
4
|
-
"version": "0.
|
|
5
|
-
"keywords": [
|
|
6
|
-
"cdp",
|
|
7
|
-
"electron",
|
|
8
|
-
"tauri",
|
|
9
|
-
"desktop",
|
|
10
|
-
"testing",
|
|
11
|
-
"accessibility",
|
|
12
|
-
"screenshot",
|
|
13
|
-
"automation"
|
|
14
|
-
],
|
|
15
|
-
"author": {
|
|
16
|
-
"name": "PetukhovArt"
|
|
17
|
-
},
|
|
18
|
-
"homepage": "https://github.com/PetukhovArt/agent-view",
|
|
19
|
-
"repository": "https://github.com/PetukhovArt/agent-view",
|
|
20
|
-
"license": "MIT"
|
|
21
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-view",
|
|
3
|
+
"description": "Visual verification CLI for Electron/Tauri/Chromium apps via Chrome DevTools Protocol. Ships with verify skill for easy and cheap checks against a live app, including heap-snapshot memory-leak debugging.",
|
|
4
|
+
"version": "0.15.0",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cdp",
|
|
7
|
+
"electron",
|
|
8
|
+
"tauri",
|
|
9
|
+
"desktop",
|
|
10
|
+
"testing",
|
|
11
|
+
"accessibility",
|
|
12
|
+
"screenshot",
|
|
13
|
+
"automation"
|
|
14
|
+
],
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "PetukhovArt"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/PetukhovArt/agent-view",
|
|
19
|
+
"repository": "https://github.com/PetukhovArt/agent-view",
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Artem Petukhov
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artem Petukhov
|
|
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
CHANGED
|
@@ -31,7 +31,7 @@ Works with any agent that can run shell commands. There's a Claude Code plugin i
|
|
|
31
31
|
|
|
32
32
|
- Reads state inside `SharedWorker`, `ServiceWorker`, and dedicated workers. Half of a modern app's state lives there, and most browser-automation tools don't follow it.
|
|
33
33
|
- Every command takes `--window <id>`. Settings, tray, and detached windows in Electron and Tauri apps work the same as the main window.
|
|
34
|
-
- Electron, Tauri (WebView2
|
|
34
|
+
- Electron, Tauri on Windows (WebView2), and plain Chromium. One CLI, same commands. Tauri on macOS/Linux embeds WebKit, which has no Chrome DevTools Protocol, so it is out of reach.
|
|
35
35
|
- `click`, `fill`, and `drag` fire real CDP input events. Vue `v-model`, React controlled inputs, and native fields actually accept the value; synthetic DOM events fail silently there.
|
|
36
36
|
- `watch` emits RFC-6902 JSON-patches of any JS expression between two events. Answers "what mutated after the click?" without parsing screenshots.
|
|
37
37
|
- `dom` returns the accessibility tree with `[ref=N]` handles. `--compact` cuts deep trees by 40–60%; `--diff`, `--count`, and `--max-lines` keep output bounded. `screenshot --crop` and WebP scaling do the same for vision tokens.
|
|
@@ -86,7 +86,7 @@ In `package.json`, wrap the dev script with [`cross-env`](https://www.npmjs.com/
|
|
|
86
86
|
}
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
Then `npm run dev` as usual. Devtools must be enabled in `tauri.conf.json` (default in `tauri dev`; for release builds, enable the `devtools` Cargo feature). macOS/Linux WebKit
|
|
89
|
+
Then `npm run dev` as usual. Devtools must be enabled in `tauri.conf.json` (default in `tauri dev`; for release builds, enable the `devtools` Cargo feature). Windows only: on macOS/Linux Tauri embeds WebKit, which speaks the WebKit Remote Inspector protocol, not CDP, so agent-view cannot attach there.
|
|
90
90
|
</details>
|
|
91
91
|
|
|
92
92
|
<details>
|
|
@@ -154,6 +154,9 @@ The daemon is why `dom → click → dom` runs in ~17ms total: one persistent CD
|
|
|
154
154
|
| `console` | `console.log` + `Log.entryAdded` per page **and per worker**, with `--follow --until <pattern>`. |
|
|
155
155
|
| `logs` | Durable file feed of page + worker console, surviving reloads and worker restarts. `tail --grep/--since/--level`, `clear`, plus re-injecting `--probe` scripts. |
|
|
156
156
|
| `network` | Request/response timeline, headers, timing, bodies, and WebSocket/SSE frames. Filters: `--url`, `--method`, `--status`, `--type`. Captures page-load traffic. |
|
|
157
|
+
| `coverage` | Which functions ran since the last `--clear`. Turns "does any action reach this code?" into a positive answer. Filters: `--filter`, `--file`, `--count`. |
|
|
158
|
+
| `listeners` | Event handlers bound to a node, each with the `file:line` it was declared at. |
|
|
159
|
+
| `heap` | Named heap snapshots diffed by class: what grew after repeating an action, how many are detached DOM nodes, and what retains them. |
|
|
157
160
|
| `upload` | Puts files into a file input — no picker opens. `--selector` reaches the hidden inputs that have no ref. |
|
|
158
161
|
| `dialog` | JS modals are answered automatically so they cannot freeze the run; sets the answer, and pre-answers native file pickers with `arm`. |
|
|
159
162
|
| `wait` | Block until an element appears (default 10s). |
|
|
@@ -264,7 +267,7 @@ npx electron-vite dev -- --remote-debugging-port=9876
|
|
|
264
267
|
|
|
265
268
|
| Runtime | Setup |
|
|
266
269
|
|----------------------|--------------------------------------------|
|
|
267
|
-
| **Tauri**
|
|
270
|
+
| **Tauri (Windows)** | `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`, see above. WebKit builds (macOS/Linux) are not supported |
|
|
268
271
|
| **Any Chromium app** | `--remote-debugging-port=9876` launch flag |
|
|
269
272
|
|
|
270
273
|
### Verify CDP is working
|
|
@@ -375,6 +378,7 @@ Clicks a DOM element by ref ID or coordinates.
|
|
|
375
378
|
agent-view click 5 # By ref from dom output
|
|
376
379
|
agent-view click --pos 100,200 # By coordinates (for canvas)
|
|
377
380
|
agent-view click 5 --double # Double-click (fires dblclick handlers)
|
|
381
|
+
agent-view click 5 --right # Right-click (fires contextmenu)
|
|
378
382
|
```
|
|
379
383
|
|
|
380
384
|
### `fill`
|
|
@@ -586,6 +590,81 @@ agent-view network --clear # drop in-memory ring
|
|
|
586
590
|
|
|
587
591
|
Sensitive headers (`Authorization`, `Cookie`, `Set-Cookie`, `X-Api-Key`, …) are redacted by default; `--raw-headers` reveals them. Request/response **bodies** stay off until the project owner sets `"captureBody": true` (bodies can carry tokens/PII). WebSocket frame payloads are visible by default — seeing them is the point — capped per frame. `--follow` and `--until` mirror `console`. `--target` / `--window` scope to one target.
|
|
588
592
|
|
|
593
|
+
### `coverage`
|
|
594
|
+
|
|
595
|
+
Reports which JavaScript functions ran since the last `--clear`, grouped by script URL. Answers the one question the DOM and the store cannot: *does any user action actually reach this code?* Built on the V8 precise-coverage delta — `--clear` resets the counters, the next call reads and resets them again.
|
|
596
|
+
|
|
597
|
+
```bash
|
|
598
|
+
agent-view coverage --clear # open a window: counters reset, counting starts
|
|
599
|
+
agent-view coverage # what ran since --clear (and reset again)
|
|
600
|
+
agent-view coverage --file "OrderForm" # only scripts whose URL contains this
|
|
601
|
+
agent-view coverage --filter "onSubmit" # function name, or script URL, contains this
|
|
602
|
+
agent-view coverage --count # just the number of executed functions
|
|
603
|
+
agent-view coverage --all # include node_modules / runtime / url-less scripts
|
|
604
|
+
agent-view coverage --max-lines 40 # cap the output, tail `… N more lines`
|
|
605
|
+
agent-view coverage --target bench-worker # a worker's own coverage window
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
The workflow is `clear → act → check`, the same shape as `console` and `network`:
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
agent-view coverage --clear && agent-view click --filter "Save" && agent-view coverage --file "OrderForm"
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Every read is also a reset.** Two `coverage` calls in a row report different things: the second one covers only what happened between them. That is what makes a single click attributable.
|
|
615
|
+
|
|
616
|
+
**Only positive answers are cheap.** A function in the output proves that this action executed it. An empty result — `(no code executed since --clear)` — proves only that *this* action did not reach the code, never that nothing can. Exit code stays 0: it is an answer, not a failure.
|
|
617
|
+
|
|
618
|
+
Granularity is the function, not the line, so no source map is involved: in a dev build the module URL already is the file path. Unnamed functions (arrows, module top level) print as `<anonymous>@<byte-offset>`, which keeps two of them apart in the same file.
|
|
619
|
+
|
|
620
|
+
`--target` gives a worker its own window — the only way to prove that SharedWorker code ran. Coverage lives in the V8 isolate, so `location.reload()` wipes it: open a new window after a reload. Reading before any `--clear` is a usage error, not an empty result.
|
|
621
|
+
|
|
622
|
+
By default `node_modules`, runtime bundles (`node:`, `chrome-extension://`), and scripts with no URL at all (`eval`, `new Function`) are hidden, with a `… N scripts hidden (--all to show)` tail. `--count` prints a single integer. `--max-lines <n>` caps the output and appends `… N more lines`, as on `dom` and `network`.
|
|
623
|
+
|
|
624
|
+
### `listeners`
|
|
625
|
+
|
|
626
|
+
Lists the event listeners bound to a DOM node, each with the file and line its handler was declared at. Answers "what is wired to this button" without reading the source.
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
agent-view listeners --filter "Save" # node by accessible name, as in `click --filter`
|
|
630
|
+
agent-view listeners --ref 12 # node by ref from `dom`
|
|
631
|
+
agent-view listeners --selector "#save" # node by CSS — reaches nodes the AX tree never exposes
|
|
632
|
+
agent-view listeners --depth -1 # include the whole subtree (CDP depth; default 0)
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Positions are printed 1-based (`file.vue:88:14`), so they paste straight into an editor or a review comment. When the handler's script cannot be resolved to a URL — code injected after the last scan, or `eval`'d code, which has no URL at all — the location falls back to `scriptId:7:88:14`.
|
|
636
|
+
|
|
637
|
+
A node with no handlers prints `(no listeners on this node)` and exits 0. A `--filter` that matches nothing is an error with the usual `No element found matching "<text>"`.
|
|
638
|
+
|
|
639
|
+
`--selector` is the escape hatch for a node the AX tree never exposes — a hidden or `aria-hidden` element has no `[ref=N]` at all, so no other flag can address it. A selector matching nothing is an error: `No element matches selector "<css>"`.
|
|
640
|
+
|
|
641
|
+
### `heap`
|
|
642
|
+
|
|
643
|
+
Takes named V8 heap snapshots and compares them by class. Answers "does repeating this action leak memory, and what holds the leaked objects?" without the multi-hundred-MB `.heapsnapshot` ever reaching the agent: the server parses it and keeps only the class table and the graph.
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
agent-view heap take --name baseline # full GC, then snapshot
|
|
647
|
+
# … repeat the suspected action ~10 times with click / fill / eval …
|
|
648
|
+
agent-view heap take --name target
|
|
649
|
+
agent-view heap diff baseline target # per-class growth, largest first
|
|
650
|
+
agent-view heap diff --detached # detached DOM nodes only
|
|
651
|
+
agent-view heap retainers "Detached <div class=\"row\">" # who holds them, one hop up
|
|
652
|
+
agent-view heap summary --filter "OrderRow" # one snapshot's classes by size
|
|
653
|
+
agent-view heap list # snapshots the server holds
|
|
654
|
+
agent-view heap clear
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
```
|
|
658
|
+
baseline → target (page:Orders): +3,201 nodes, +2.4 MB, detached +240 (+960 KB)
|
|
659
|
+
class Δcount Δsize count
|
|
660
|
+
Detached <div class="row"> +240 +960 KB 252
|
|
661
|
+
OrderRowVM +10 +12 KB 10
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
A `Δcount` that is a multiple of the repetition count is the leak; `+3` is noise. `retainers` names the object and property holding the instances (`Array []`, `RowRegistry .el`), counting distinct instances and skipping weak edges. One hop only: retained sizes, dominators and full retaining paths are not computed, and for those the app's own DevTools Memory panel shows the same class names on the same CDP port.
|
|
665
|
+
|
|
666
|
+
Snapshots live in the lazy server and vanish when it idles out (5 min) or on `clear`. `--target <worker>` snapshots a worker's own heap. The full method, with a table of what each diff shape usually means, is in [`skills/verify/references/memory-leaks.md`](skills/verify/references/memory-leaks.md).
|
|
667
|
+
|
|
589
668
|
### `watch`
|
|
590
669
|
|
|
591
670
|
Polls a JS expression and streams JSON-patch (RFC 6902) diffs as it changes. Closes the "what changed between click and final state?" gap that screenshots and DOM dumps can't cover. **Requires `"allowEval": true`** (same gate as `eval`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/cdp/transport.ts"],"names":[],"mappings":"AAEA,OAAO,EAaL,KAAK,SAAS,EAId,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/cdp/transport.ts"],"names":[],"mappings":"AAEA,OAAO,EAaL,KAAK,SAAS,EAId,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAcjB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AA4BhD,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAmCD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAkBpE;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQ9E;AAyyBD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CA+BhG;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,WAAW,CAAC,CAqVtB"}
|
package/dist/cdp/transport.js
CHANGED
|
@@ -627,6 +627,100 @@ function attachDisconnectSubscription(client) {
|
|
|
627
627
|
handlers.add(handler);
|
|
628
628
|
};
|
|
629
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Coverage is a delta, not a total: `takePreciseCoverage` resets the counters, so
|
|
632
|
+
* the window between two takes is attributable to exactly what happened in it.
|
|
633
|
+
* `detailed: false` keeps the granularity at the function — enough to name a
|
|
634
|
+
* handler, and free of the source-map remapping that byte ranges would need.
|
|
635
|
+
*/
|
|
636
|
+
function attachCoverage(client) {
|
|
637
|
+
const Profiler = client.Profiler;
|
|
638
|
+
let started = false;
|
|
639
|
+
return {
|
|
640
|
+
async start() {
|
|
641
|
+
if (!started) {
|
|
642
|
+
await Profiler.enable();
|
|
643
|
+
await Profiler.startPreciseCoverage({ callCount: true, detailed: false });
|
|
644
|
+
started = true;
|
|
645
|
+
}
|
|
646
|
+
await Profiler.takePreciseCoverage(); // discarded — this take *is* the clear
|
|
647
|
+
},
|
|
648
|
+
async take() {
|
|
649
|
+
if (!started)
|
|
650
|
+
return null;
|
|
651
|
+
const { result } = await Profiler.takePreciseCoverage();
|
|
652
|
+
const scripts = [];
|
|
653
|
+
for (const script of result) {
|
|
654
|
+
const functions = [];
|
|
655
|
+
for (const fn of script.functions) {
|
|
656
|
+
const count = Math.max(0, ...fn.ranges.map((r) => r.count));
|
|
657
|
+
if (count === 0)
|
|
658
|
+
continue;
|
|
659
|
+
functions.push({ name: fn.functionName, offset: fn.ranges[0]?.startOffset ?? 0, count });
|
|
660
|
+
}
|
|
661
|
+
if (functions.length > 0)
|
|
662
|
+
scripts.push({ scriptId: script.scriptId, url: script.url, functions });
|
|
663
|
+
}
|
|
664
|
+
return scripts;
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/** Chunks arrive before the command resolves, so subscribing first and joining after is the whole protocol. */
|
|
669
|
+
async function takeHeapSnapshotImpl(client) {
|
|
670
|
+
const HeapProfiler = client.HeapProfiler;
|
|
671
|
+
const chunks = [];
|
|
672
|
+
const unsubscribe = HeapProfiler.addHeapSnapshotChunk(({ chunk }) => { chunks.push(chunk); });
|
|
673
|
+
try {
|
|
674
|
+
await HeapProfiler.takeHeapSnapshot({ reportProgress: false, captureNumericValue: false });
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
unsubscribe();
|
|
678
|
+
}
|
|
679
|
+
return chunks.join('');
|
|
680
|
+
}
|
|
681
|
+
/** Bounds the wait for the `scriptParsed` replay when an id is still missing. */
|
|
682
|
+
const SCRIPT_PARSED_DEADLINE_MS = 1_000;
|
|
683
|
+
const SCRIPT_PARSED_POLL_MS = 20;
|
|
684
|
+
/**
|
|
685
|
+
* `Debugger.scriptParsed` is replayed for every already-parsed script the moment
|
|
686
|
+
* `Debugger.enable` lands, and it is the only CDP event carrying scriptId → URL.
|
|
687
|
+
* The domain is enabled only for the length of that replay: leaving it on keeps
|
|
688
|
+
* V8 deoptimized for every other command on the session, and nothing here needs
|
|
689
|
+
* a breakpoint.
|
|
690
|
+
*
|
|
691
|
+
* The map accumulates, so ids already known cost nothing and a scan runs only
|
|
692
|
+
* when one misses. The wait is a gate on the ids actually asked for, not a fixed
|
|
693
|
+
* pause — a fixed pause is either too short on a large app (missing URLs) or
|
|
694
|
+
* wasted on every call. An id still missing at the deadline stays unresolved,
|
|
695
|
+
* which the formatter prints as a raw `scriptId`.
|
|
696
|
+
*/
|
|
697
|
+
function attachScriptUrls(client) {
|
|
698
|
+
const urls = new Map();
|
|
699
|
+
// Subscribe before any enable, per the transport-wide rule.
|
|
700
|
+
client.on('Debugger.scriptParsed', ((p) => {
|
|
701
|
+
urls.set(p.scriptId, p.url ?? '');
|
|
702
|
+
}));
|
|
703
|
+
const allKnown = (wanted) => wanted.every((id) => urls.has(id));
|
|
704
|
+
return async (wanted) => {
|
|
705
|
+
if (allKnown(wanted))
|
|
706
|
+
return urls;
|
|
707
|
+
const Debug = client.Debugger;
|
|
708
|
+
try {
|
|
709
|
+
await Debug.enable();
|
|
710
|
+
const deadline = Date.now() + SCRIPT_PARSED_DEADLINE_MS;
|
|
711
|
+
while (!allKnown(wanted) && Date.now() < deadline) {
|
|
712
|
+
await new Promise((r) => setTimeout(r, SCRIPT_PARSED_POLL_MS));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
try {
|
|
717
|
+
await Debug.disable();
|
|
718
|
+
}
|
|
719
|
+
catch { /* target went away mid-scan */ }
|
|
720
|
+
}
|
|
721
|
+
return urls;
|
|
722
|
+
};
|
|
723
|
+
}
|
|
630
724
|
export async function connectToRuntime(port, target) {
|
|
631
725
|
if (process.env.AV_DEBUG_CONSOLE) {
|
|
632
726
|
// eslint-disable-next-line no-console
|
|
@@ -637,6 +731,7 @@ export async function connectToRuntime(port, target) {
|
|
|
637
731
|
const consoleSub = attachConsoleSubscription(client);
|
|
638
732
|
const networkSub = attachNetworkSubscription(client.Network);
|
|
639
733
|
const onDisconnect = attachDisconnectSubscription(client);
|
|
734
|
+
const coverage = attachCoverage(client);
|
|
640
735
|
await bringUpSession(client, target, async () => {
|
|
641
736
|
await client.Runtime.enable();
|
|
642
737
|
await client.Log.enable();
|
|
@@ -649,6 +744,9 @@ export async function connectToRuntime(port, target) {
|
|
|
649
744
|
onNetwork: (handler) => networkSub.add(handler),
|
|
650
745
|
enableNetwork: () => networkSub.enable(),
|
|
651
746
|
getResponseBody: (requestId) => networkSub.getResponseBody(requestId),
|
|
747
|
+
startCoverage: () => coverage.start(),
|
|
748
|
+
takeCoverage: () => coverage.take(),
|
|
749
|
+
takeHeapSnapshot: () => takeHeapSnapshotImpl(client),
|
|
652
750
|
async close() {
|
|
653
751
|
await client.close();
|
|
654
752
|
},
|
|
@@ -664,6 +762,7 @@ export async function connectToPage(port, target, cache) {
|
|
|
664
762
|
}
|
|
665
763
|
const client = await openClient(port, target);
|
|
666
764
|
const { Runtime, Accessibility, Page, DOM, Input } = client;
|
|
765
|
+
const DOMDebugger = client.DOMDebugger;
|
|
667
766
|
const cacheKey = `${port}:${target.id}`;
|
|
668
767
|
// Subscribe BEFORE enable so we catch buffered console/log entries emitted at enable-time.
|
|
669
768
|
const consoleSub = attachConsoleSubscription(client);
|
|
@@ -677,6 +776,8 @@ export async function connectToPage(port, target, cache) {
|
|
|
677
776
|
});
|
|
678
777
|
}, consoleSub.emit);
|
|
679
778
|
const onDisconnect = attachDisconnectSubscription(client);
|
|
779
|
+
const coverage = attachCoverage(client);
|
|
780
|
+
const scanScriptUrls = attachScriptUrls(client);
|
|
680
781
|
let documentBackendNodeId = 0;
|
|
681
782
|
await bringUpSession(client, target, async () => {
|
|
682
783
|
await Page.enable();
|
|
@@ -703,15 +804,16 @@ export async function connectToPage(port, target, cache) {
|
|
|
703
804
|
});
|
|
704
805
|
async function dispatchClick(x, y, opts) {
|
|
705
806
|
const clicks = Math.max(1, opts?.clicks ?? 1);
|
|
807
|
+
const button = opts?.button ?? MouseButton.Left;
|
|
706
808
|
if (clicks === 1) {
|
|
707
|
-
const pressed = Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button
|
|
708
|
-
const released = Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button
|
|
809
|
+
const pressed = Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button, clickCount: 1 });
|
|
810
|
+
const released = Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button, clickCount: 1 });
|
|
709
811
|
await Promise.all([pressed, released]);
|
|
710
812
|
return;
|
|
711
813
|
}
|
|
712
814
|
for (let i = 1; i <= clicks; i++) {
|
|
713
|
-
await Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button
|
|
714
|
-
await Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button
|
|
815
|
+
await Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button, clickCount: i });
|
|
816
|
+
await Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button, clickCount: i });
|
|
715
817
|
}
|
|
716
818
|
}
|
|
717
819
|
async function dispatchDrag(from, to, opts) {
|
|
@@ -753,10 +855,10 @@ export async function connectToPage(port, target, cache) {
|
|
|
753
855
|
const { object } = await DOM.resolveNode({ backendNodeId });
|
|
754
856
|
const climbed = await Runtime.callFunctionOn({
|
|
755
857
|
objectId: object.objectId,
|
|
756
|
-
functionDeclaration: `function(levels) {
|
|
757
|
-
let el = this.nodeType === Node.TEXT_NODE ? this.parentElement : this;
|
|
758
|
-
for (let i = 0; i < levels && el.parentElement; i++) el = el.parentElement;
|
|
759
|
-
return el;
|
|
858
|
+
functionDeclaration: `function(levels) {
|
|
859
|
+
let el = this.nodeType === Node.TEXT_NODE ? this.parentElement : this;
|
|
860
|
+
for (let i = 0; i < levels && el.parentElement; i++) el = el.parentElement;
|
|
861
|
+
return el;
|
|
760
862
|
}`,
|
|
761
863
|
arguments: [{ value: levels }],
|
|
762
864
|
});
|
|
@@ -778,6 +880,22 @@ export async function connectToPage(port, target, cache) {
|
|
|
778
880
|
const minY = Math.min(...ys);
|
|
779
881
|
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
780
882
|
}
|
|
883
|
+
async function listenersOn(objectId, depth) {
|
|
884
|
+
const { listeners } = await DOMDebugger.getEventListeners({ objectId, depth });
|
|
885
|
+
if (listeners.length === 0)
|
|
886
|
+
return [];
|
|
887
|
+
const urls = await scanScriptUrls(listeners.map((l) => l.scriptId));
|
|
888
|
+
return listeners.map((l) => ({
|
|
889
|
+
type: l.type,
|
|
890
|
+
useCapture: Boolean(l.useCapture),
|
|
891
|
+
passive: Boolean(l.passive),
|
|
892
|
+
once: Boolean(l.once),
|
|
893
|
+
scriptId: l.scriptId,
|
|
894
|
+
url: urls.get(l.scriptId) ?? '',
|
|
895
|
+
lineNumber: l.lineNumber ?? 0,
|
|
896
|
+
columnNumber: l.columnNumber ?? 0,
|
|
897
|
+
}));
|
|
898
|
+
}
|
|
781
899
|
return {
|
|
782
900
|
target,
|
|
783
901
|
evaluate: (expression, opts) => evaluateImpl(client, expression, opts),
|
|
@@ -786,6 +904,27 @@ export async function connectToPage(port, target, cache) {
|
|
|
786
904
|
onNetwork: (handler) => networkSub.add(handler),
|
|
787
905
|
enableNetwork: () => networkSub.enable(),
|
|
788
906
|
getResponseBody: (requestId) => networkSub.getResponseBody(requestId),
|
|
907
|
+
startCoverage: () => coverage.start(),
|
|
908
|
+
takeCoverage: () => coverage.take(),
|
|
909
|
+
takeHeapSnapshot: () => takeHeapSnapshotImpl(client),
|
|
910
|
+
async getEventListeners(backendDOMNodeId, depth = 0) {
|
|
911
|
+
const { object } = await DOM.resolveNode({ backendNodeId: backendDOMNodeId });
|
|
912
|
+
return listenersOn(object.objectId, depth);
|
|
913
|
+
},
|
|
914
|
+
async getEventListenersBySelector(selector, depth = 0) {
|
|
915
|
+
// objectId, not nodeId: `DOM.querySelector` needs a document nodeId, which is
|
|
916
|
+
// renumbered on every navigation. Same reasoning as `uploadBySelector`.
|
|
917
|
+
const { result, exceptionDetails } = await Runtime.evaluate({
|
|
918
|
+
expression: `document.querySelector(${JSON.stringify(selector)})`,
|
|
919
|
+
returnByValue: false,
|
|
920
|
+
});
|
|
921
|
+
// A malformed selector throws a DOMException that arrives as a valid RemoteObject.
|
|
922
|
+
if (exceptionDetails)
|
|
923
|
+
return null;
|
|
924
|
+
if (!result.objectId || result.subtype === 'null')
|
|
925
|
+
return null;
|
|
926
|
+
return listenersOn(result.objectId, depth);
|
|
927
|
+
},
|
|
789
928
|
onJsDialog: (handler) => jsDialogSub.add(handler),
|
|
790
929
|
answerJsDialog: (accept, promptText) => jsDialogSub.answer(accept, promptText),
|
|
791
930
|
setJsDialogPolicy: (policy) => jsDialogSub.setPolicy(policy),
|
|
@@ -907,13 +1046,13 @@ export async function connectToPage(port, target, cache) {
|
|
|
907
1046
|
await DOM.focus({ backendNodeId });
|
|
908
1047
|
await Runtime.callFunctionOn({
|
|
909
1048
|
objectId: object.objectId,
|
|
910
|
-
functionDeclaration: `function(val) {
|
|
911
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
|
|
912
|
-
|| Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
913
|
-
if (nativeSetter) nativeSetter.call(this, val);
|
|
914
|
-
else this.value = val;
|
|
915
|
-
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
916
|
-
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1049
|
+
functionDeclaration: `function(val) {
|
|
1050
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
|
|
1051
|
+
|| Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
1052
|
+
if (nativeSetter) nativeSetter.call(this, val);
|
|
1053
|
+
else this.value = val;
|
|
1054
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1055
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
917
1056
|
}`,
|
|
918
1057
|
arguments: [{ value }],
|
|
919
1058
|
});
|