cdp-toolkit 2.5.1 → 2.7.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/CHANGELOG.md +30 -0
- package/CONTRACT.md +1 -1
- package/README.md +3 -3
- package/dist/bidi/mux-daemon.js +47 -0
- package/dist/cli.js +97 -10
- package/dist/index.js +73 -4
- package/dist/mcp.js +190 -30
- package/package.json +2 -1
- package/src/bidi/driver.ts +8 -0
- package/src/cdp/driver.ts +69 -4
- package/src/cli.ts +29 -7
- package/src/client.ts +22 -0
- package/src/driver.ts +2 -1
- package/src/frames.ts +169 -0
- package/src/manifest.ts +1 -1
- package/src/mcp.ts +200 -49
- package/src/shared-tools.ts +13 -0
- package/src/toolDocs.ts +3 -3
- package/src/types.ts +13 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to cdp-toolkit are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.7.0] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
**Cross-origin iframes are now discoverable and addressable.** A visible out-of-process iframe used to be a silent hole: `take_snapshot` on the parent page stopped at a bare `Iframe` line with no hint that an entire interactable document lived behind it, `list_pages` hid the iframe's target by default, and the only way to drive it was to know that a raw target id from `list_pages {all:true}` happened to work. Now the snapshot marks the boundary, the docs say the target exists, and a `frame:` selector addresses it by URL. Verified live against Chrome 153 with a real cross-origin iframe: marker present in both snapshot modes, `evaluate_script`/`take_snapshot` drive the frame through both its target id and the new selector, and two iframes with the same URL produce an ambiguity error naming both ids instead of a silent first-match.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **OOPIF markers in `take_snapshot`.** When an emitted `<iframe>` node owns an out-of-process (cross-origin) frame, its snapshot line now carries the frame's identity: `[4821] Iframe "Checkout" [frame="F405BF…" frameUrl="https://example.com/"]`. `frame` is the frame id, which IS the iframe's CDP target id — feed it straight back as `target` to any interaction tool. The marker appears in `interactiveOnly` mode too (an iframe at a process boundary is itself the interactable thing), while same-origin iframes are unchanged: their AX subtrees were already merged into the parent tree. Detection drives off the browser's target list correlated through `DOM.getFrameOwner` — deliberately not `Page.getFrameTree`, which omits out-of-process child frames on a page-level attachment (observed Chrome 153) — and is failure-soft end to end: any error in detection degrades to the pre-marker snapshot, never a failed call. (`src/cdp/driver.ts`, `test/iframe-markers.test.ts`)
|
|
15
|
+
- **`frame:<url-substring>` target selector (Chrome).** Resolves against the browser's out-of-process iframe targets by URL substring, so an iframe is addressable without first correlating raw target ids out of `list_pages {all:true}`. Follows the `worker:` arm's rules: an empty needle matches nothing, more than one match is an ambiguity error naming every id+url (never a silent first-match), and a miss lists the live iframes and teaches that only out-of-process iframes appear as targets. Accepted by every tool whose `target` flows through the shared resolver (evaluate_script, take_snapshot, click, screenshot, …); `close_page`/`select_page`/`release_page`/`claim_page` refuse it with a page-only message, and Firefox refuses it with `unsupported` naming the `frame.targets` capability, same as `worker:`. A bare iframe target id still resolves everywhere it did before. (`src/frames.ts`, `src/client.ts`, `src/shared-tools.ts`, `src/bidi/driver.ts`, `test/frame-selector.test.ts`)
|
|
16
|
+
|
|
17
|
+
### Documented
|
|
18
|
+
|
|
19
|
+
- `list_pages {all:true}` now says it lists out-of-process iframes (not just workers/background pages) and that a returned non-page targetId is accepted as a bare id by any tool's `target` param — in the tool's deep doc, the manifest, the MCP server instructions, the README tools table and target-grammar bullet, the CLI help grammar, and the CONTRACT grammar rule. The snapshot doc names the cross-origin boundary and the marker. (`src/toolDocs.ts`, `src/mcp.ts`, `src/manifest.ts`, `README.md`, `CONTRACT.md`, `skills/using-cdp-toolkit/SKILL.md`)
|
|
20
|
+
|
|
21
|
+
### Deferred
|
|
22
|
+
|
|
23
|
+
- Recursing child-frame accessibility nodes into the parent snapshot is NOT in this release. `backendDOMNodeId` uids are per-renderer-process, so child-frame uids would collide with parent uids under every interaction tool's `DOM.resolveNode` — recursion requires session-aware uid namespacing across all nine interaction tools, plus a BiDi-side answer where no AX domain exists. The marker + `frame:` pair ships the discoverability without that surgery.
|
|
24
|
+
|
|
25
|
+
## [2.6.0] - 2026-09-15
|
|
26
|
+
|
|
27
|
+
**Bun is now a first-class, CI-tested runtime, and the MCP server can serve streamable HTTP.** The package has always declared `engines.bun`, but nothing proved the server actually booted under Bun — or under Node, for that matter. This release pins both: the server and CLI run under Bun ≥ 1.1 or Node ≥ 22, importing `dist/mcp.js` / `dist/cli.js` as libraries is now silent (no server start, no CLI usage dump), and under Bun the same `mcp.js` entry can serve the full MCP surface over loopback HTTP with `--transport streamable-http` for hosts that launch it as an HTTP service instead of a stdio child.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **`--transport streamable-http` (Bun only).** `mcp.js` accepts `--transport <stdio|streamable-http>` (default `stdio`, byte-identical behavior to previous releases), plus `--port` (default 3000; `0` picks an OS port, announced on the stderr listening line) and `--host` (default 127.0.0.1). The HTTP path is built on the MCP SDK 2.0's Web-standard `createMcpHandler` behind the same `buildServer()` factory stdio uses, so both transports can never drift apart; loopback host-header and origin validation are enforced in front of it. Launched under Node with `--transport streamable-http`, the server fails fast with a clear stderr message rather than half-serving. The listening URL goes to stderr only — stdout remains the stdio JSON-RPC channel. (`src/mcp.ts`)
|
|
32
|
+
- **CI `runtime` job that gates the badge.** Boots the BUILT `dist/mcp.js` under both `node` (22, matching the `engines.node` floor) and `bun` on every push/PR: silent-import checks for `dist/mcp.js` and `dist/cli.js` under both runtimes, a hand-rolled stdio JSON-RPC `initialize` + `tools/list` handshake under both runtimes (no SDK client, so the wire is what's asserted), and a streamable-http `initialize` round-trip under Bun. No browser involved, so every failure is a real regression. New `bun run test:runtime` runs the same suite locally (`test/runtime-entry.test.ts`, 7 tests). (`.github/workflows/ci.yml`)
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **Importing the entries is now silent under both runtimes.** `dist/mcp.js` and `dist/cli.js` used to start serving / print usage the moment any bundler or `require.resolve`-style host imported them. Both files now gate on a direct-run check (`realpathSync(process.argv[1])` against `import.meta.url`, which handles bin symlinks under npm/bunx and works identically under Node and Bun, ESM-only): only direct/bin invocation runs the server or CLI; library import is side-effect free. `buildServer` is exported from `src/mcp.ts` for embedders. (`src/mcp.ts`, `src/cli.ts`)
|
|
37
|
+
|
|
8
38
|
## [2.5.1] - 2026-09-12
|
|
9
39
|
|
|
10
40
|
### Fixed
|
package/CONTRACT.md
CHANGED
|
@@ -20,7 +20,7 @@ writing a module.
|
|
|
20
20
|
|
|
21
21
|
**A worker-typed hit inside `resolveTarget` is the other deliberate exception, added in 1.9.0.** A `worker:<substring>` selector (Chrome only, capability `worker.targets`; one tool in 1.9.0, `evaluate_script` — four as of 1.9.1, see `WORKER_CAPABLE_TOOLS` in `src/workers.ts`) resolves to a service/shared worker target, and `resolveTarget` returns it before reaching `assertLeaseOk` — see the comment at the top of that branch in `src/client.ts`. A lease answers "who is driving this tab"; a worker is not a tab, has no reap-meaningful close, and strict-mode auto-acquire on it would mint a lease file keyed on an id that vanishes the moment Chrome idles the worker out, garbage the page-only reap set (`ReapInput.livePageIds`) can never collect. This also cannot be used to bypass a lease on an actual tab: there is no tab a worker resolution could stand in for. **The three console/network reader tools take a parallel path, not this one** — `resolveRecorderTarget`/`resolveWorkerSelector` in `src/tools/recorder.ts`/`src/cdp/workers.ts` — but the same reasoning holds: neither touches `assertLeaseOk`, tested by asserting zero lease files on a worker read with a page control in the same strict-mode window.
|
|
22
22
|
|
|
23
|
-
**The `TargetSelector` grammar itself also has three independent copies, a separate fact from the lease choke points above** — `pickTarget` (`src/client.ts`, what `resolveTarget` calls, and what most tools reach through), `pickPage` (`src/shared-tools.ts`), and `pickContext` (`src/bidi/driver.ts`, Firefox). A prefix arm added to one does not exist for callers routed through another; `label:` (1.9.0) was first wired into only `pickPage` and passed every unit test while `evaluate_script` still rejected it, because `evaluate_script` calls `pickTarget`, not `pickPage`, and only the live smoke caught the gap. The shared lookup core for an arm that needs identical semantics everywhere (as `label:` does) belongs in its own module — see `resolveLiveLabel` in `src/origins.ts` — wired into all three call sites, each keeping its own error class (`CdpError` / `SharedToolError` / `DriverError`). `test/manifest-grammar-drift.test.ts` structurally asserts a completeness property (every `title:` mention also mentions `label:`) and, separately, an exclusion property for `worker:` (only a tool in the `WORKER_CAPABLE_TOOLS` allowlist — four as of 1.9.1 — may name it) — grep `startsWith("title:")` across `src/` before adding a new arm, and extend or scope that test for it.
|
|
23
|
+
**The `TargetSelector` grammar itself also has three independent copies, a separate fact from the lease choke points above** — `pickTarget` (`src/client.ts`, what `resolveTarget` calls, and what most tools reach through), `pickPage` (`src/shared-tools.ts`), and `pickContext` (`src/bidi/driver.ts`, Firefox). A prefix arm added to one does not exist for callers routed through another; `label:` (1.9.0) was first wired into only `pickPage` and passed every unit test while `evaluate_script` still rejected it, because `evaluate_script` calls `pickTarget`, not `pickPage`, and only the live smoke caught the gap. The shared lookup core for an arm that needs identical semantics everywhere (as `label:` does) belongs in its own module — see `resolveLiveLabel` in `src/origins.ts` — wired into all three call sites, each keeping its own error class (`CdpError` / `SharedToolError` / `DriverError`). `test/manifest-grammar-drift.test.ts` structurally asserts a completeness property (every `title:` mention also mentions `label:`) and, separately, an exclusion property for `worker:` (only a tool in the `WORKER_CAPABLE_TOOLS` allowlist — four as of 1.9.1 — may name it) — grep `startsWith("title:")` across `src/` before adding a new arm, and extend or scope that test for it. `frame:<substr>` (OOPIF iframes) is Chrome-only like `worker:` but resolves through all three parsers with one deliberate asymmetry: an iframe targetId from an unfiltered `list_pages{all:true}` resolves as a bare 32-hex id everywhere, while `worker:` never matches an iframe.
|
|
24
24
|
|
|
25
25
|
7. **Never dispatch `Input.*` outside `sendInput`.** As of 1.8.0 there is exactly one function that writes a synthesized input command to a Chrome connection: `sendInput(conn, backend, targetId, method, params)` in `src/activity.ts`. `CdpPageDriver.dispatchInput` (the per-driver choke point every interaction method already had to go through) delegates to it, and any tool built outside the Driver abstraction (`dispatch_mouse`, in `src/tools/dispatch-mouse.ts`, is the precedent) must call it directly rather than opening its own `conn.send("Input....", ...)`. This is what keeps the activity beacon's human/agent discrimination (`src/activity.ts`'s dispatch log) correct: a new input path that bypasses `sendInput` will typecheck, pass its own unit tests, and pass `bun test` in full, while silently making the toolkit's own input read as a human using the tab — exactly the 1.8.0 merge bug `test/input-dispatch-wiring.test.ts` now exists to catch (it walks every `.ts` under `src/`, strips comment-only lines, and fails on any line that both sends and names `Input.`). On the Firefox side, `BidiPageDriver.performActions` is the twin choke point and already calls `recordDispatch`; a new BiDi input path goes through it the same way `CdpPageDriver.dispatchInput` is used on Chrome. Mode-toggle commands that dispatch no page input themselves (`Input.setInterceptDrags`'s arm/disarm around HTML5 drag) are still sent through `sendInput` but are on a denylist that skips recording them — classify a new such command explicitly; the default for anything unclassified is to be logged, since over-warning a human is recoverable and silently misattributing their input to "agent" is not.
|
|
26
26
|
|
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ npx -y --package cdp-toolkit cdp navigate_page --target index:0 --url https://ex
|
|
|
87
87
|
# …or from a clone: `bun run src/cli.ts <tool> …`
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
**Requirements:** Node ≥ 22 **or** Bun ≥ 1.1: `npx -y cdp-toolkit` and `bunx -y cdp-toolkit` both work (the published bins are plain Node ESM). Chrome/Chromium with `--remote-debugging-port=9222`. Smoke-check the port: `curl -s http://127.0.0.1:9222/json/version`.
|
|
90
|
+
**Requirements:** Node ≥ 22 **or** Bun ≥ 1.1: `npx -y cdp-toolkit` and `bunx -y cdp-toolkit` both work (the published bins are plain Node ESM, and the server + CLI are CI-tested under both runtimes). Chrome/Chromium with `--remote-debugging-port=9222`. Smoke-check the port: `curl -s http://127.0.0.1:9222/json/version`. Under Bun only, `--transport streamable-http` (with `--port`/`--host`, default `127.0.0.1:3000`) serves the same MCP surface over loopback HTTP instead of stdio.
|
|
91
91
|
|
|
92
92
|
### MCP client setup
|
|
93
93
|
|
|
@@ -140,7 +140,7 @@ bun run mcp:smoke # spawn the server + a real initialize/tools-list/tools-call
|
|
|
140
140
|
|
|
141
141
|
Each point below leads with the symptom you've probably hit, then the cause, then the fix.
|
|
142
142
|
|
|
143
|
-
- **Your agent stalls on a call when a busy background tab is open** → that's the **all-target fan-out**: every operation broadcasts to all attached targets. every cdp-toolkit call resolves *one* target (`active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>`) and attaches a single WebSocket to just that page, so a busy tab you did not name is never touched.
|
|
143
|
+
- **Your agent stalls on a call when a busy background tab is open** → that's the **all-target fan-out**: every operation broadcasts to all attached targets. every cdp-toolkit call resolves *one* target (`active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>`, plus `frame:<substr>` for an out-of-process iframe, Chrome-only) and attaches a single WebSocket to just that page, so a busy tab you did not name is never touched.
|
|
144
144
|
- **A tool hangs forever and never returns** → the MCP's eager `Network.enable` on a busy or hung renderer is a known wedge. cdp-toolkit enables domains **lazily**, only where a tool needs them (the recorder enables `Network`/`Runtime`/`Log`; most tools touch only `Page`/`Runtime`/`DOM`).
|
|
145
145
|
- **No way to bound a slow call** → `CdpConnection.send()` enforces `CDP_TIMEOUT_MS` (15s default) on *every* command and **rejects rather than hangs**, so a stuck renderer can never block a caller indefinitely.
|
|
146
146
|
- **Element handles drift across calls** → a `uid` **is** a CDP `backendDOMNodeId`, resolved on demand via `DOM.resolveNode`. There is no server-side handle table to drift or expire.
|
|
@@ -434,7 +434,7 @@ The 29 parity tools are 1:1 with `chrome-devtools-mcp`; the 19 superset tools (`
|
|
|
434
434
|
|
|
435
435
|
| MCP name | CDP method(s) | Parity notes / gaps |
|
|
436
436
|
|---|---|---|
|
|
437
|
-
| `list_pages` | `GET /json/list` | `all` flag also exposes worker/background targets; MCP lists only page tabs. Each row additionally carries `origin` (`agent` or `unknown`, never `human`) plus `label`/`createdAt` for tabs this toolkit created, and, for a tab under an active lease of this backend, a `lease:{label,pid,idleMs,expiresAt,stale}` field (unconditional on `probe`). `probe:true` pings each page-type target's renderer (one bounded 500ms `Runtime.evaluate`, never more) and adds `responsive:boolean` plus `humanActiveMs` where that round trip found human-attributed input; a wedged/unreachable tab reports `responsive:false`, never an error for the whole call. Under `CDP_REQUIRE_LEASE` also reaps abandoned agent tabs first (destructively, only once a lease is `CDP_REAP_GRACE_MS` past its TTL — see "Staleness" below), reporting closures in an additive `reaped` array. |
|
|
437
|
+
| `list_pages` | `GET /json/list` | `all` flag also exposes worker/background and out-of-process (OOPIF) iframe targets; MCP lists only page tabs. A non-page targetId from this listing is accepted by any tool's `target` param as a bare id, and `take_snapshot` marks a cross-origin iframe node with `frame:"<targetId>"` (input inside such an iframe is invisible to the activity beacon — see "Staleness" below — but the frame itself is fully driveable as its own target). Each row additionally carries `origin` (`agent` or `unknown`, never `human`) plus `label`/`createdAt` for tabs this toolkit created, and, for a tab under an active lease of this backend, a `lease:{label,pid,idleMs,expiresAt,stale}` field (unconditional on `probe`). `probe:true` pings each page-type target's renderer (one bounded 500ms `Runtime.evaluate`, never more) and adds `responsive:boolean` plus `humanActiveMs` where that round trip found human-attributed input; a wedged/unreachable tab reports `responsive:false`, never an error for the whole call. Under `CDP_REQUIRE_LEASE` also reaps abandoned agent tabs first (destructively, only once a lease is `CDP_REAP_GRACE_MS` past its TTL — see "Staleness" below), reporting closures in an additive `reaped` array. |
|
|
438
438
|
| `new_page` | `Target.createTarget` (+ lease file) | Returns `{targetId,url}`; does not await navigation (use `navigate_page`). `claim:true` also claims the new tab and returns a `lease` token (`label`/`ttlMs` optional). Under `CDP_REQUIRE_LEASE` the tab is claimed and a `lease` returned even without `claim:true`. |
|
|
439
439
|
| `close_page` | `Target.closeTarget` (+ lease file) | Reports `success:true` on the empty result newer Chromium returns. A successful close also releases that tab's lease; a failed close leaves it in place. |
|
|
440
440
|
| `select_page` | `Target.activateTarget` + selected-state file | Writes a flat-file selected target; `resolveTarget` does not read it, so `active` still means `index:0` unless a tool opts in. |
|
package/dist/bidi/mux-daemon.js
CHANGED
|
@@ -689,6 +689,37 @@ var init_workers = __esm(() => {
|
|
|
689
689
|
WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless). The tools that accept a worker: target are " + `${WORKER_CAPABLE_TOOLS.join(", ")}.`;
|
|
690
690
|
});
|
|
691
691
|
|
|
692
|
+
// src/frames.ts
|
|
693
|
+
function isFrameSelector(selector) {
|
|
694
|
+
return typeof selector === "string" && selector.startsWith(FRAME_SELECTOR_PREFIX);
|
|
695
|
+
}
|
|
696
|
+
function frameNeedle(selector) {
|
|
697
|
+
return selector.slice(FRAME_SELECTOR_PREFIX.length);
|
|
698
|
+
}
|
|
699
|
+
function isFrameTargetType(type) {
|
|
700
|
+
return type === FRAME_TARGET_TYPE;
|
|
701
|
+
}
|
|
702
|
+
function resolveFrameTargets(targets, needle) {
|
|
703
|
+
const liveFrames = targets.filter((t) => isFrameTargetType(t.type));
|
|
704
|
+
return {
|
|
705
|
+
matches: needle === "" ? [] : liveFrames.filter((t) => t.url.includes(needle)),
|
|
706
|
+
liveFrames
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function frameAmbiguityMessage(needle, matches) {
|
|
710
|
+
return `'frame:${needle}' matches more than one out-of-process iframe: ` + matches.map((m) => `${m.url} (${m.id})`).join(", ") + ". Narrow the substring, or pass the iframe's target id directly.";
|
|
711
|
+
}
|
|
712
|
+
function frameMissMessage(needle, liveFrames) {
|
|
713
|
+
const live = liveFrames.length ? ` (out-of-process iframes: ${liveFrames.map((f) => f.url).join(", ")})` : " (no out-of-process iframes are live)";
|
|
714
|
+
return `no out-of-process iframe whose url contains '${needle}'${live}. ` + "Only a CROSS-ORIGIN iframe gets its own process and therefore its own target; a same-origin iframe " + "runs inside its parent page and is NOT a target at all, so no substring can ever select it — reach a " + "same-origin iframe through its parent page instead (evaluate_script on the page, then the iframe " + "element's contentDocument). An out-of-process iframe whose document navigated away is likewise gone " + "from the listing: re-list with list_pages{all:true} and pass the new target id.";
|
|
715
|
+
}
|
|
716
|
+
var FRAME_SELECTOR_PREFIX = "frame:", FRAME_PAGE_ONLY_TOOLS, FRAME_TARGET_TYPE = "iframe", FRAME_EMPTY_NEEDLE_MESSAGE = "'frame:' needs a substring of the iframe's url, e.g. frame:ads.example.com or frame:player.html", FRAME_SELECTOR_UNSUPPORTED_MESSAGE, FRAME_SELECTOR_PAGE_ONLY_MESSAGE;
|
|
717
|
+
var init_frames = __esm(() => {
|
|
718
|
+
FRAME_PAGE_ONLY_TOOLS = ["close_page", "select_page", "release_page", "claim_page"];
|
|
719
|
+
FRAME_SELECTOR_UNSUPPORTED_MESSAGE = "target 'frame:<substring>' selects an out-of-process iframe, which this backend cannot address " + "(WebDriver BiDi has no iframe target — an iframe is a realm inside its tab's browsing context, not a " + "selectable target; Capability 'frame.targets' is Chrome-only). Use --browser chrome to act on a cross-origin iframe.";
|
|
720
|
+
FRAME_SELECTOR_PAGE_ONLY_MESSAGE = "target 'frame:<substring>' selects an out-of-process iframe, and this tool is page-only " + "(closing, selecting, releasing or claiming an iframe is meaningless; these act on the embedder tab). " + `The page-only tools are ${FRAME_PAGE_ONLY_TOOLS.join(", ")}; every other tool that takes a 'target' ` + "accepts a frame: selector.";
|
|
721
|
+
});
|
|
722
|
+
|
|
692
723
|
// src/activity.ts
|
|
693
724
|
function dispatchKey(backend, targetId) {
|
|
694
725
|
return `${backend}:${targetId}`;
|
|
@@ -2127,6 +2158,17 @@ async function pickTarget(targets, selector) {
|
|
|
2127
2158
|
return matches[0];
|
|
2128
2159
|
throw new CdpError(workerMissMessage(needle, { wakeAttempted: false, liveWorkers }));
|
|
2129
2160
|
}
|
|
2161
|
+
if (isFrameSelector(selector)) {
|
|
2162
|
+
const needle = frameNeedle(selector);
|
|
2163
|
+
if (needle === "")
|
|
2164
|
+
throw new CdpError(FRAME_EMPTY_NEEDLE_MESSAGE);
|
|
2165
|
+
const { matches, liveFrames } = resolveFrameTargets(targets, needle);
|
|
2166
|
+
if (matches.length > 1)
|
|
2167
|
+
throw new CdpError(frameAmbiguityMessage(needle, matches));
|
|
2168
|
+
if (matches.length === 1)
|
|
2169
|
+
return matches[0];
|
|
2170
|
+
throw new CdpError(frameMissMessage(needle, liveFrames));
|
|
2171
|
+
}
|
|
2130
2172
|
const byId = targets.find((t) => t.id === selector);
|
|
2131
2173
|
if (!byId)
|
|
2132
2174
|
throw new CdpError(`no target with id '${selector}'`);
|
|
@@ -2153,6 +2195,7 @@ var init_client2 = __esm(() => {
|
|
|
2153
2195
|
init_leases();
|
|
2154
2196
|
init_origins();
|
|
2155
2197
|
init_workers();
|
|
2198
|
+
init_frames();
|
|
2156
2199
|
BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
2157
2200
|
DEFAULT_TIMEOUT_MS3 = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
|
|
2158
2201
|
CdpError = class CdpError extends Error {
|
|
@@ -2843,6 +2886,9 @@ async function pickContext(conn, contexts, selector) {
|
|
|
2843
2886
|
if (isWorkerSelector(selector)) {
|
|
2844
2887
|
throw driverError("unsupported", WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
2845
2888
|
}
|
|
2889
|
+
if (isFrameSelector(selector)) {
|
|
2890
|
+
throw driverError("unsupported", FRAME_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
2891
|
+
}
|
|
2846
2892
|
const exact = contexts.find((c) => c.context === selector);
|
|
2847
2893
|
if (exact)
|
|
2848
2894
|
return exact;
|
|
@@ -3054,6 +3100,7 @@ var init_driver2 = __esm(() => {
|
|
|
3054
3100
|
init_leases();
|
|
3055
3101
|
init_origins();
|
|
3056
3102
|
init_workers();
|
|
3103
|
+
init_frames();
|
|
3057
3104
|
init_activity();
|
|
3058
3105
|
init_client();
|
|
3059
3106
|
init_session_lease();
|
package/dist/cli.js
CHANGED
|
@@ -473,6 +473,37 @@ var init_workers = __esm(() => {
|
|
|
473
473
|
WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless). The tools that accept a worker: target are " + `${WORKER_CAPABLE_TOOLS.join(", ")}.`;
|
|
474
474
|
});
|
|
475
475
|
|
|
476
|
+
// src/frames.ts
|
|
477
|
+
function isFrameSelector(selector) {
|
|
478
|
+
return typeof selector === "string" && selector.startsWith(FRAME_SELECTOR_PREFIX);
|
|
479
|
+
}
|
|
480
|
+
function frameNeedle(selector) {
|
|
481
|
+
return selector.slice(FRAME_SELECTOR_PREFIX.length);
|
|
482
|
+
}
|
|
483
|
+
function isFrameTargetType(type) {
|
|
484
|
+
return type === FRAME_TARGET_TYPE;
|
|
485
|
+
}
|
|
486
|
+
function resolveFrameTargets(targets, needle) {
|
|
487
|
+
const liveFrames = targets.filter((t) => isFrameTargetType(t.type));
|
|
488
|
+
return {
|
|
489
|
+
matches: needle === "" ? [] : liveFrames.filter((t) => t.url.includes(needle)),
|
|
490
|
+
liveFrames
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function frameAmbiguityMessage(needle, matches) {
|
|
494
|
+
return `'frame:${needle}' matches more than one out-of-process iframe: ` + matches.map((m) => `${m.url} (${m.id})`).join(", ") + ". Narrow the substring, or pass the iframe's target id directly.";
|
|
495
|
+
}
|
|
496
|
+
function frameMissMessage(needle, liveFrames) {
|
|
497
|
+
const live = liveFrames.length ? ` (out-of-process iframes: ${liveFrames.map((f) => f.url).join(", ")})` : " (no out-of-process iframes are live)";
|
|
498
|
+
return `no out-of-process iframe whose url contains '${needle}'${live}. ` + "Only a CROSS-ORIGIN iframe gets its own process and therefore its own target; a same-origin iframe " + "runs inside its parent page and is NOT a target at all, so no substring can ever select it — reach a " + "same-origin iframe through its parent page instead (evaluate_script on the page, then the iframe " + "element's contentDocument). An out-of-process iframe whose document navigated away is likewise gone " + "from the listing: re-list with list_pages{all:true} and pass the new target id.";
|
|
499
|
+
}
|
|
500
|
+
var FRAME_SELECTOR_PREFIX = "frame:", FRAME_PAGE_ONLY_TOOLS, FRAME_TARGET_TYPE = "iframe", FRAME_EMPTY_NEEDLE_MESSAGE = "'frame:' needs a substring of the iframe's url, e.g. frame:ads.example.com or frame:player.html", FRAME_SELECTOR_UNSUPPORTED_MESSAGE, FRAME_SELECTOR_PAGE_ONLY_MESSAGE;
|
|
501
|
+
var init_frames = __esm(() => {
|
|
502
|
+
FRAME_PAGE_ONLY_TOOLS = ["close_page", "select_page", "release_page", "claim_page"];
|
|
503
|
+
FRAME_SELECTOR_UNSUPPORTED_MESSAGE = "target 'frame:<substring>' selects an out-of-process iframe, which this backend cannot address " + "(WebDriver BiDi has no iframe target — an iframe is a realm inside its tab's browsing context, not a " + "selectable target; Capability 'frame.targets' is Chrome-only). Use --browser chrome to act on a cross-origin iframe.";
|
|
504
|
+
FRAME_SELECTOR_PAGE_ONLY_MESSAGE = "target 'frame:<substring>' selects an out-of-process iframe, and this tool is page-only " + "(closing, selecting, releasing or claiming an iframe is meaningless; these act on the embedder tab). " + `The page-only tools are ${FRAME_PAGE_ONLY_TOOLS.join(", ")}; every other tool that takes a 'target' ` + "accepts a frame: selector.";
|
|
505
|
+
});
|
|
506
|
+
|
|
476
507
|
// src/client.ts
|
|
477
508
|
class CdpConnection {
|
|
478
509
|
wsUrl;
|
|
@@ -671,6 +702,17 @@ async function pickTarget(targets, selector) {
|
|
|
671
702
|
return matches[0];
|
|
672
703
|
throw new CdpError(workerMissMessage(needle, { wakeAttempted: false, liveWorkers }));
|
|
673
704
|
}
|
|
705
|
+
if (isFrameSelector(selector)) {
|
|
706
|
+
const needle = frameNeedle(selector);
|
|
707
|
+
if (needle === "")
|
|
708
|
+
throw new CdpError(FRAME_EMPTY_NEEDLE_MESSAGE);
|
|
709
|
+
const { matches, liveFrames } = resolveFrameTargets(targets, needle);
|
|
710
|
+
if (matches.length > 1)
|
|
711
|
+
throw new CdpError(frameAmbiguityMessage(needle, matches));
|
|
712
|
+
if (matches.length === 1)
|
|
713
|
+
return matches[0];
|
|
714
|
+
throw new CdpError(frameMissMessage(needle, liveFrames));
|
|
715
|
+
}
|
|
674
716
|
const byId = targets.find((t) => t.id === selector);
|
|
675
717
|
if (!byId)
|
|
676
718
|
throw new CdpError(`no target with id '${selector}'`);
|
|
@@ -697,6 +739,7 @@ var init_client = __esm(() => {
|
|
|
697
739
|
init_leases();
|
|
698
740
|
init_origins();
|
|
699
741
|
init_workers();
|
|
742
|
+
init_frames();
|
|
700
743
|
BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
701
744
|
DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
|
|
702
745
|
CdpError = class CdpError extends Error {
|
|
@@ -2844,6 +2887,9 @@ async function pickContext(conn, contexts, selector) {
|
|
|
2844
2887
|
if (isWorkerSelector(selector)) {
|
|
2845
2888
|
throw driverError2("unsupported", WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
2846
2889
|
}
|
|
2890
|
+
if (isFrameSelector(selector)) {
|
|
2891
|
+
throw driverError2("unsupported", FRAME_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
2892
|
+
}
|
|
2847
2893
|
const exact = contexts.find((c) => c.context === selector);
|
|
2848
2894
|
if (exact)
|
|
2849
2895
|
return exact;
|
|
@@ -3055,6 +3101,7 @@ var init_driver2 = __esm(() => {
|
|
|
3055
3101
|
init_leases();
|
|
3056
3102
|
init_origins();
|
|
3057
3103
|
init_workers();
|
|
3104
|
+
init_frames();
|
|
3058
3105
|
init_activity();
|
|
3059
3106
|
init_client2();
|
|
3060
3107
|
init_session_lease();
|
|
@@ -4162,7 +4209,8 @@ var CDP_CAPABILITIES = new Set([
|
|
|
4162
4209
|
"emulate.focus",
|
|
4163
4210
|
"browser.downloads",
|
|
4164
4211
|
"browser.permissions",
|
|
4165
|
-
"worker.targets"
|
|
4212
|
+
"worker.targets",
|
|
4213
|
+
"frame.targets"
|
|
4166
4214
|
]);
|
|
4167
4215
|
|
|
4168
4216
|
class CdpPageDriver {
|
|
@@ -4295,6 +4343,7 @@ class CdpPageDriver {
|
|
|
4295
4343
|
const interactiveOnly = opts?.interactiveOnly ?? false;
|
|
4296
4344
|
await this.ensureDomain("Accessibility");
|
|
4297
4345
|
const { nodes } = await this.conn.send("Accessibility.getFullAXTree");
|
|
4346
|
+
const oopifFrames = await this.detectOopifFrames();
|
|
4298
4347
|
const byId = new Map;
|
|
4299
4348
|
for (const n of nodes)
|
|
4300
4349
|
byId.set(n.nodeId, n);
|
|
@@ -4303,11 +4352,17 @@ class CdpPageDriver {
|
|
|
4303
4352
|
const walk = (node, depth) => {
|
|
4304
4353
|
const role = axString(node.role);
|
|
4305
4354
|
const name = axString(node.name);
|
|
4306
|
-
const
|
|
4307
|
-
const
|
|
4355
|
+
const backendDOMNodeId = node.backendDOMNodeId;
|
|
4356
|
+
const marker = backendDOMNodeId !== undefined ? oopifFrames.get(backendDOMNodeId) : undefined;
|
|
4357
|
+
const emitted = !node.ignored && backendDOMNodeId !== undefined && (interactiveOnly ? isInteractive(role) || marker !== undefined : !!(role || name));
|
|
4308
4358
|
if (emitted) {
|
|
4309
4359
|
const extras = axExtras(node);
|
|
4310
|
-
|
|
4360
|
+
if (marker) {
|
|
4361
|
+
extras.frame = marker.frameId;
|
|
4362
|
+
if (marker.frameUrl)
|
|
4363
|
+
extras.frameUrl = marker.frameUrl;
|
|
4364
|
+
}
|
|
4365
|
+
out.push({ uid: encodeUid(backendDOMNodeId), role: role ?? "generic", depth, ...name ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} });
|
|
4311
4366
|
}
|
|
4312
4367
|
const childDepth = emitted && !interactiveOnly ? depth + 1 : depth;
|
|
4313
4368
|
for (const childId of node.childIds ?? []) {
|
|
@@ -4320,6 +4375,20 @@ class CdpPageDriver {
|
|
|
4320
4375
|
walk(root, 0);
|
|
4321
4376
|
return out;
|
|
4322
4377
|
}
|
|
4378
|
+
async detectOopifFrames() {
|
|
4379
|
+
const markers = new Map;
|
|
4380
|
+
try {
|
|
4381
|
+
await this.ensureDomainSoft("DOM");
|
|
4382
|
+
const iframeTargets = (await listTargets()).filter((t) => t.type === "iframe");
|
|
4383
|
+
for (const target of iframeTargets) {
|
|
4384
|
+
const owner = await this.conn.send("DOM.getFrameOwner", { frameId: target.id }).catch(() => ({ backendNodeId: undefined }));
|
|
4385
|
+
if (typeof owner.backendNodeId === "number") {
|
|
4386
|
+
markers.set(owner.backendNodeId, { frameId: target.id, ...target.url ? { frameUrl: target.url } : {} });
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
} catch {}
|
|
4390
|
+
return markers;
|
|
4391
|
+
}
|
|
4323
4392
|
async locate(loc) {
|
|
4324
4393
|
return encodeUid(await backendNodeIdOf(this.conn, loc));
|
|
4325
4394
|
}
|
|
@@ -5497,6 +5566,7 @@ init_leases();
|
|
|
5497
5566
|
init_activity();
|
|
5498
5567
|
init_origins();
|
|
5499
5568
|
init_workers();
|
|
5569
|
+
init_frames();
|
|
5500
5570
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
5501
5571
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
5502
5572
|
|
|
@@ -5553,6 +5623,9 @@ async function pickPage(driver, pages, selector) {
|
|
|
5553
5623
|
if (isWorkerSelector(selector)) {
|
|
5554
5624
|
throw new SharedToolError(backendOf(driver) === "firefox" ? WORKER_SELECTOR_UNSUPPORTED_MESSAGE : WORKER_SELECTOR_PAGE_ONLY_MESSAGE);
|
|
5555
5625
|
}
|
|
5626
|
+
if (isFrameSelector(selector)) {
|
|
5627
|
+
throw new SharedToolError(backendOf(driver) === "firefox" ? FRAME_SELECTOR_UNSUPPORTED_MESSAGE : FRAME_SELECTOR_PAGE_ONLY_MESSAGE);
|
|
5628
|
+
}
|
|
5556
5629
|
const exactPage = pages.find((x) => x.id === selector);
|
|
5557
5630
|
if (exactPage)
|
|
5558
5631
|
return exactPage;
|
|
@@ -8863,7 +8936,7 @@ var MANIFEST = [
|
|
|
8863
8936
|
},
|
|
8864
8937
|
all: {
|
|
8865
8938
|
type: "boolean",
|
|
8866
|
-
description: "Include non-page targets."
|
|
8939
|
+
description: "Include non-page targets (workers, iframes)."
|
|
8867
8940
|
},
|
|
8868
8941
|
probe: {
|
|
8869
8942
|
type: "boolean",
|
|
@@ -11229,6 +11302,18 @@ Warning: could not find the ${browser} binary at its expected location. The alia
|
|
|
11229
11302
|
}
|
|
11230
11303
|
|
|
11231
11304
|
// src/cli.ts
|
|
11305
|
+
import { realpathSync } from "node:fs";
|
|
11306
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
11307
|
+
function isDirectRun() {
|
|
11308
|
+
const entry = process.argv[1];
|
|
11309
|
+
if (!entry)
|
|
11310
|
+
return false;
|
|
11311
|
+
try {
|
|
11312
|
+
return fileURLToPath2(import.meta.url) === realpathSync(entry);
|
|
11313
|
+
} catch {
|
|
11314
|
+
return false;
|
|
11315
|
+
}
|
|
11316
|
+
}
|
|
11232
11317
|
var USAGE = `cdp-toolkit: raw single-target CDP, 29-tool chrome-devtools-mcp parity, plus a Firefox backend over WebDriver BiDi.
|
|
11233
11318
|
|
|
11234
11319
|
Usage:
|
|
@@ -11466,9 +11551,11 @@ async function main() {
|
|
|
11466
11551
|
}
|
|
11467
11552
|
});
|
|
11468
11553
|
}
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11554
|
+
if (isDirectRun()) {
|
|
11555
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
11556
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11557
|
+
process.stderr.write(`${JSON.stringify({ error: message })}
|
|
11472
11558
|
`);
|
|
11473
|
-
|
|
11474
|
-
});
|
|
11559
|
+
process.exit(1);
|
|
11560
|
+
});
|
|
11561
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -472,6 +472,37 @@ var init_workers = __esm(() => {
|
|
|
472
472
|
WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless). The tools that accept a worker: target are " + `${WORKER_CAPABLE_TOOLS.join(", ")}.`;
|
|
473
473
|
});
|
|
474
474
|
|
|
475
|
+
// src/frames.ts
|
|
476
|
+
function isFrameSelector(selector) {
|
|
477
|
+
return typeof selector === "string" && selector.startsWith(FRAME_SELECTOR_PREFIX);
|
|
478
|
+
}
|
|
479
|
+
function frameNeedle(selector) {
|
|
480
|
+
return selector.slice(FRAME_SELECTOR_PREFIX.length);
|
|
481
|
+
}
|
|
482
|
+
function isFrameTargetType(type) {
|
|
483
|
+
return type === FRAME_TARGET_TYPE;
|
|
484
|
+
}
|
|
485
|
+
function resolveFrameTargets(targets, needle) {
|
|
486
|
+
const liveFrames = targets.filter((t) => isFrameTargetType(t.type));
|
|
487
|
+
return {
|
|
488
|
+
matches: needle === "" ? [] : liveFrames.filter((t) => t.url.includes(needle)),
|
|
489
|
+
liveFrames
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function frameAmbiguityMessage(needle, matches) {
|
|
493
|
+
return `'frame:${needle}' matches more than one out-of-process iframe: ` + matches.map((m) => `${m.url} (${m.id})`).join(", ") + ". Narrow the substring, or pass the iframe's target id directly.";
|
|
494
|
+
}
|
|
495
|
+
function frameMissMessage(needle, liveFrames) {
|
|
496
|
+
const live = liveFrames.length ? ` (out-of-process iframes: ${liveFrames.map((f) => f.url).join(", ")})` : " (no out-of-process iframes are live)";
|
|
497
|
+
return `no out-of-process iframe whose url contains '${needle}'${live}. ` + "Only a CROSS-ORIGIN iframe gets its own process and therefore its own target; a same-origin iframe " + "runs inside its parent page and is NOT a target at all, so no substring can ever select it — reach a " + "same-origin iframe through its parent page instead (evaluate_script on the page, then the iframe " + "element's contentDocument). An out-of-process iframe whose document navigated away is likewise gone " + "from the listing: re-list with list_pages{all:true} and pass the new target id.";
|
|
498
|
+
}
|
|
499
|
+
var FRAME_SELECTOR_PREFIX = "frame:", FRAME_PAGE_ONLY_TOOLS, FRAME_TARGET_TYPE = "iframe", FRAME_EMPTY_NEEDLE_MESSAGE = "'frame:' needs a substring of the iframe's url, e.g. frame:ads.example.com or frame:player.html", FRAME_SELECTOR_UNSUPPORTED_MESSAGE, FRAME_SELECTOR_PAGE_ONLY_MESSAGE;
|
|
500
|
+
var init_frames = __esm(() => {
|
|
501
|
+
FRAME_PAGE_ONLY_TOOLS = ["close_page", "select_page", "release_page", "claim_page"];
|
|
502
|
+
FRAME_SELECTOR_UNSUPPORTED_MESSAGE = "target 'frame:<substring>' selects an out-of-process iframe, which this backend cannot address " + "(WebDriver BiDi has no iframe target — an iframe is a realm inside its tab's browsing context, not a " + "selectable target; Capability 'frame.targets' is Chrome-only). Use --browser chrome to act on a cross-origin iframe.";
|
|
503
|
+
FRAME_SELECTOR_PAGE_ONLY_MESSAGE = "target 'frame:<substring>' selects an out-of-process iframe, and this tool is page-only " + "(closing, selecting, releasing or claiming an iframe is meaningless; these act on the embedder tab). " + `The page-only tools are ${FRAME_PAGE_ONLY_TOOLS.join(", ")}; every other tool that takes a 'target' ` + "accepts a frame: selector.";
|
|
504
|
+
});
|
|
505
|
+
|
|
475
506
|
// src/client.ts
|
|
476
507
|
class CdpConnection {
|
|
477
508
|
wsUrl;
|
|
@@ -670,6 +701,17 @@ async function pickTarget(targets, selector) {
|
|
|
670
701
|
return matches[0];
|
|
671
702
|
throw new CdpError(workerMissMessage(needle, { wakeAttempted: false, liveWorkers }));
|
|
672
703
|
}
|
|
704
|
+
if (isFrameSelector(selector)) {
|
|
705
|
+
const needle = frameNeedle(selector);
|
|
706
|
+
if (needle === "")
|
|
707
|
+
throw new CdpError(FRAME_EMPTY_NEEDLE_MESSAGE);
|
|
708
|
+
const { matches, liveFrames } = resolveFrameTargets(targets, needle);
|
|
709
|
+
if (matches.length > 1)
|
|
710
|
+
throw new CdpError(frameAmbiguityMessage(needle, matches));
|
|
711
|
+
if (matches.length === 1)
|
|
712
|
+
return matches[0];
|
|
713
|
+
throw new CdpError(frameMissMessage(needle, liveFrames));
|
|
714
|
+
}
|
|
673
715
|
const byId = targets.find((t) => t.id === selector);
|
|
674
716
|
if (!byId)
|
|
675
717
|
throw new CdpError(`no target with id '${selector}'`);
|
|
@@ -696,6 +738,7 @@ var init_client = __esm(() => {
|
|
|
696
738
|
init_leases();
|
|
697
739
|
init_origins();
|
|
698
740
|
init_workers();
|
|
741
|
+
init_frames();
|
|
699
742
|
BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
700
743
|
DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
|
|
701
744
|
CdpError = class CdpError extends Error {
|
|
@@ -1564,7 +1607,8 @@ var CDP_CAPABILITIES = new Set([
|
|
|
1564
1607
|
"emulate.focus",
|
|
1565
1608
|
"browser.downloads",
|
|
1566
1609
|
"browser.permissions",
|
|
1567
|
-
"worker.targets"
|
|
1610
|
+
"worker.targets",
|
|
1611
|
+
"frame.targets"
|
|
1568
1612
|
]);
|
|
1569
1613
|
|
|
1570
1614
|
class CdpPageDriver {
|
|
@@ -1697,6 +1741,7 @@ class CdpPageDriver {
|
|
|
1697
1741
|
const interactiveOnly = opts?.interactiveOnly ?? false;
|
|
1698
1742
|
await this.ensureDomain("Accessibility");
|
|
1699
1743
|
const { nodes } = await this.conn.send("Accessibility.getFullAXTree");
|
|
1744
|
+
const oopifFrames = await this.detectOopifFrames();
|
|
1700
1745
|
const byId = new Map;
|
|
1701
1746
|
for (const n of nodes)
|
|
1702
1747
|
byId.set(n.nodeId, n);
|
|
@@ -1705,11 +1750,17 @@ class CdpPageDriver {
|
|
|
1705
1750
|
const walk = (node, depth) => {
|
|
1706
1751
|
const role = axString(node.role);
|
|
1707
1752
|
const name = axString(node.name);
|
|
1708
|
-
const
|
|
1709
|
-
const
|
|
1753
|
+
const backendDOMNodeId = node.backendDOMNodeId;
|
|
1754
|
+
const marker = backendDOMNodeId !== undefined ? oopifFrames.get(backendDOMNodeId) : undefined;
|
|
1755
|
+
const emitted = !node.ignored && backendDOMNodeId !== undefined && (interactiveOnly ? isInteractive(role) || marker !== undefined : !!(role || name));
|
|
1710
1756
|
if (emitted) {
|
|
1711
1757
|
const extras = axExtras(node);
|
|
1712
|
-
|
|
1758
|
+
if (marker) {
|
|
1759
|
+
extras.frame = marker.frameId;
|
|
1760
|
+
if (marker.frameUrl)
|
|
1761
|
+
extras.frameUrl = marker.frameUrl;
|
|
1762
|
+
}
|
|
1763
|
+
out.push({ uid: encodeUid(backendDOMNodeId), role: role ?? "generic", depth, ...name ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} });
|
|
1713
1764
|
}
|
|
1714
1765
|
const childDepth = emitted && !interactiveOnly ? depth + 1 : depth;
|
|
1715
1766
|
for (const childId of node.childIds ?? []) {
|
|
@@ -1722,6 +1773,20 @@ class CdpPageDriver {
|
|
|
1722
1773
|
walk(root, 0);
|
|
1723
1774
|
return out;
|
|
1724
1775
|
}
|
|
1776
|
+
async detectOopifFrames() {
|
|
1777
|
+
const markers = new Map;
|
|
1778
|
+
try {
|
|
1779
|
+
await this.ensureDomainSoft("DOM");
|
|
1780
|
+
const iframeTargets = (await listTargets()).filter((t) => t.type === "iframe");
|
|
1781
|
+
for (const target of iframeTargets) {
|
|
1782
|
+
const owner = await this.conn.send("DOM.getFrameOwner", { frameId: target.id }).catch(() => ({ backendNodeId: undefined }));
|
|
1783
|
+
if (typeof owner.backendNodeId === "number") {
|
|
1784
|
+
markers.set(owner.backendNodeId, { frameId: target.id, ...target.url ? { frameUrl: target.url } : {} });
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
} catch {}
|
|
1788
|
+
return markers;
|
|
1789
|
+
}
|
|
1725
1790
|
async locate(loc) {
|
|
1726
1791
|
return encodeUid(await backendNodeIdOf(this.conn, loc));
|
|
1727
1792
|
}
|
|
@@ -2899,6 +2964,7 @@ init_leases();
|
|
|
2899
2964
|
init_activity();
|
|
2900
2965
|
init_origins();
|
|
2901
2966
|
init_workers();
|
|
2967
|
+
init_frames();
|
|
2902
2968
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2903
2969
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
2904
2970
|
|
|
@@ -2955,6 +3021,9 @@ async function pickPage(driver, pages, selector) {
|
|
|
2955
3021
|
if (isWorkerSelector(selector)) {
|
|
2956
3022
|
throw new SharedToolError(backendOf(driver) === "firefox" ? WORKER_SELECTOR_UNSUPPORTED_MESSAGE : WORKER_SELECTOR_PAGE_ONLY_MESSAGE);
|
|
2957
3023
|
}
|
|
3024
|
+
if (isFrameSelector(selector)) {
|
|
3025
|
+
throw new SharedToolError(backendOf(driver) === "firefox" ? FRAME_SELECTOR_UNSUPPORTED_MESSAGE : FRAME_SELECTOR_PAGE_ONLY_MESSAGE);
|
|
3026
|
+
}
|
|
2958
3027
|
const exactPage = pages.find((x) => x.id === selector);
|
|
2959
3028
|
if (exactPage)
|
|
2960
3029
|
return exactPage;
|