diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index aac4de2..e2e2b0f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -42,7 +42,7 @@ jobs:
run: |
case "${{ matrix.client }}" in
js)
- xvfb-run -a pnpm exec vitest run \
+ pnpm exec vitest run \
$(find js/test -name 'test.*.ts' \
! -name 'test.ModCDPClient.ts' \
! -name 'test.NatsUpstreamTransport.ts' \
@@ -52,7 +52,7 @@ jobs:
;;
python)
cd python
- xvfb-run -a uv run python -m unittest \
+ uv run python -m unittest \
$(find tests -name 'test_*.py' \
! -name 'test_ModCDPClient.py' \
! -name 'test_NatsUpstreamTransport.py' \
@@ -61,7 +61,7 @@ jobs:
;;
go)
cd go
- xvfb-run -a go test -count=1 -p 1 \
+ go test -count=1 -p 1 \
./modcdp \
./modcdp/injector \
./modcdp/launcher \
@@ -122,15 +122,15 @@ jobs:
run: |
case "${{ matrix.client }}" in
js)
- xvfb-run -a node dist/js/examples/demo.js --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
+ node dist/js/examples/demo.js --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
;;
python)
cd python
- xvfb-run -a uv run python examples/demo.py --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
+ uv run python examples/demo.py --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
;;
go)
cd go
- xvfb-run -a go run ./examples/demo --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
+ go run ./examples/demo --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
;;
*)
echo "unknown client: ${{ matrix.client }}" >&2
@@ -164,25 +164,41 @@ jobs:
cache-dependency-path: go/go.sum
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- - name: Run JS serialized connector tests
+ - name: Run JS serialized non-reverse connector tests
run: |
- xvfb-run -a pnpm exec vitest run \
+ pnpm exec vitest run \
js/test/test.ModCDPClient.ts \
js/test/test.NatsUpstreamTransport.ts \
+ js/test/test.proxy.ts \
+ --testNamePattern "^(?!.*reversews).*" \
+ --fileParallelism=false --maxWorkers=1
+ - name: Run JS serialized reversews tests
+ run: |
+ pnpm exec vitest run \
js/test/test.ReverseWebSocketUpstreamTransport.ts \
js/test/test.proxy.ts \
+ --testNamePattern "reversews" \
--fileParallelism=false --maxWorkers=1
- - name: Run Python serialized connector tests
+ - name: Run Python serialized non-reverse connector tests
run: |
cd python
- xvfb-run -a uv run python -m unittest \
+ uv run python -m unittest \
tests.test_ModCDPClient \
- tests.test_NatsUpstreamTransport \
+ tests.test_NatsUpstreamTransport
+ - name: Run Python serialized reversews tests
+ run: |
+ cd python
+ uv run python -m unittest \
tests.test_ReverseWebSocketUpstreamTransport
- - name: Run Go serialized connector tests
+ - name: Run Go serialized non-reverse connector tests
+ run: |
+ cd go
+ go test -count=1 -p 1 ./modcdp/client
+ go test -count=1 -p 1 ./modcdp/transport -run 'Test(UpstreamTransport|WebSocketUpstreamTransport|PipeUpstreamTransport|NativeMessagingUpstreamTransport|NatsUpstreamTransport)'
+ - name: Run Go serialized reversews tests
run: |
cd go
- xvfb-run -a go test -count=1 -p 1 ./modcdp/client ./modcdp/transport
+ go test -count=1 -p 1 ./modcdp/transport -run 'TestReverseWebSocketUpstreamTransport'
serialized-reversews-demo:
name: serialized reversews demos
@@ -213,12 +229,12 @@ jobs:
- name: Run reversews demos serially
run: |
for mode in loopback debugger; do
- xvfb-run -a node dist/js/examples/demo.js --"$mode" --upstream=reversews
+ node dist/js/examples/demo.js --"$mode" --upstream=reversews
cd python
- xvfb-run -a uv run python examples/demo.py --"$mode" --upstream=reversews
+ uv run python examples/demo.py --"$mode" --upstream=reversews
cd ..
cd go
- xvfb-run -a go run ./examples/demo --"$mode" --upstream=reversews
+ go run ./examples/demo --"$mode" --upstream=reversews
cd ..
done
@@ -247,4 +263,4 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- - run: xvfb-run -a ${{ matrix.command }}
+ - run: ${{ matrix.command }}
diff --git a/README.md b/README.md
index 1967245..82bb61c 100644
--- a/README.md
+++ b/README.md
@@ -150,12 +150,13 @@ Native messaging mode creates the local native host wrapper and browser manifest
Use reverse mode when the browser does not expose a public CDP websocket to the final client, but the ModCDP extension can open a websocket back to a local proxy. The proxy still serves a normal-looking CDP endpoint to Playwright, Puppeteer, Stagehand, or any other CDP client:
```sh
-pnpm run proxy -- --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --listen 127.0.0.1:9223
-pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --port 9223
+pnpm run proxy -- --upstream-mode=reversews --port 9223
+pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --port 9223
+pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29293 --port 9223
# const browser = await playwright.chromium.connectOverCDP("http://127.0.0.1:9223")
```
-Reverse mode is opt-in. The shipped extension has no generated runtime config; it always tries the fixed local reverse connector at `ws://127.0.0.1:29292`. With `--launcher-mode=local`, use that default bind and the normal `ModCDPClient` launcher, injector, and `ReverseWebSocketUpstreamTransport` path will wake the service worker and accept its connection. With `--launcher-mode=none` or a non-default bind, an already-running extension or test must explicitly call `globalThis.ModCDP.startReverseBridge("ws://host:port", { reconnect_interval_ms: 2000 })` with the chosen endpoint because there is no side channel before the reverse connection exists. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits for that extension connection. Once it connects, it self-identifies as a ModCDP extension service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker.
+Reverse mode is opt-in. The shipped extension auto-connects to the fixed local reverse connector at `ws://127.0.0.1:29292`; the proxy/client listens there and waits for that extension connection. Keep `--upstream-reversews-bind` when using a custom extension build whose compiled autoconnect URL points at a different host or port. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits. Once connected, the extension identifies itself as a ModCDP service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker.
Reverse mode is intentionally scoped to one local browser and one reverse extension connection per proxy process. The browser may still have other extensions installed; ModCDP does not require `--disable-extensions-except`.
@@ -230,7 +231,7 @@ dist/ Built JS output used by the extension and Node CLI scr
3. Attach a session to that SW target and `Runtime.enable` on it.
4. Call `globalThis.ModCDP.configure(...)` to push the resolved loopback websocket and any explicit server route overrides into the SW. The clients do this automatically by default.
-Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292` listens for the extension, while still serving downstream clients from `--listen`. The extension artifact is fixed, so automatic launch/injection uses the default `127.0.0.1:29292` connector; non-default reverse binds need an external bootstrap call to `globalThis.ModCDP.startReverseBridge(...)` with the same URL. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client.
+Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --port 9223` listens for the extension on the configured reverse connector while still serving downstream clients from the proxy port. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client.
### Send
diff --git a/TODO_layout.md b/TODO_layout.md
index aa73d53..329c418 100644
--- a/TODO_layout.md
+++ b/TODO_layout.md
@@ -16,8 +16,6 @@ repo/
pages/
options.html
options.ts
- wake.html
- wake.ts
offscreen_keepalive.html
offscreen_keepalive.ts
diff --git a/extension/src/pages/options.html b/extension/src/pages/options.html
index ec32164..57c97cb 100644
--- a/extension/src/pages/options.html
+++ b/extension/src/pages/options.html
@@ -21,4 +21,4 @@
Refresh
ModCDP
Loading...
-
+
diff --git a/extension/src/pages/wake.html b/extension/src/pages/wake.html
deleted file mode 100644
index 014ae62..0000000
--- a/extension/src/pages/wake.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
- ModCDP Wake
-
-
-
-
diff --git a/extension/src/pages/wake.ts b/extension/src/pages/wake.ts
deleted file mode 100644
index ad8db44..0000000
--- a/extension/src/pages/wake.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-chrome.runtime.sendMessage({ type: "modcdp.wake", at: Date.now() }, () => {
- setTimeout(() => window.close(), 250);
-});
diff --git a/extension/src/service_worker.ts b/extension/src/service_worker.ts
index 4c9fcb4..954b81c 100644
--- a/extension/src/service_worker.ts
+++ b/extension/src/service_worker.ts
@@ -92,7 +92,13 @@ const downstreamClient = (session_id?: string | null) => {
last_seen: at,
});
const id = session_id || "root";
- const session = (client.sessions[id] ??= { id, commands: 0, events: 0, first_seen: at, last_seen: at });
+ const session = (client.sessions[id] ??= {
+ id,
+ commands: 0,
+ events: 0,
+ first_seen: at,
+ last_seen: at,
+ });
return { at, client_id, client, session };
};
const logTraffic = (direction: "command" | "event", name: string, payload: unknown, session_id?: string | null) => {
@@ -156,7 +162,11 @@ if (bridge) {
if (start) {
bridge[method] = (...args: unknown[]) => {
const result = start(...args);
- self_transports[key] = { args: compact(args), result: compact(result), updated_at: new Date().toISOString() };
+ self_transports[key] = {
+ args: compact(args),
+ result: compact(result),
+ updated_at: new Date().toISOString(),
+ };
return result;
};
}
@@ -190,15 +200,6 @@ chrome.runtime.onInstalled.addListener(startConfiguredTransports);
chrome.runtime.onStartup.addListener(startConfiguredTransports);
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
- if (message?.type === "modcdp.wake") {
- startConfiguredTransports();
- sendResponse({
- ok: true,
- extension_id: chrome.runtime.id,
- service_worker_url: chrome.runtime.getURL("modcdp/service_worker.js"),
- });
- return false;
- }
if (message?.type !== "modcdp.options.status") return false;
const self = {
id: "self",
@@ -221,7 +222,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
native_bridge_last_error: bridge.native_bridge_last_error,
},
...(Object.keys(self_transports).length ? { transports: self_transports } : {}),
- custom: { commands: [...self_custom.commands], events: [...self_custom.events] },
+ custom: {
+ commands: [...self_custom.commands],
+ events: [...self_custom.events],
+ },
log: self_log,
};
sendResponse({
@@ -240,7 +244,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
},
}
: {}),
- debugger: { ...upstream_servers.debugger, id: "debugger", log: upstream_servers.debugger?.log ?? [] },
+ debugger: {
+ ...upstream_servers.debugger,
+ id: "debugger",
+ log: upstream_servers.debugger?.log ?? [],
+ },
},
});
return false;
diff --git a/go/examples/demo/main.go b/go/examples/demo/main.go
index 21afb8f..27b010d 100644
--- a/go/examples/demo/main.go
+++ b/go/examples/demo/main.go
@@ -29,11 +29,23 @@ import (
"golang.org/x/term"
)
+const reverseTransportWaitTimeoutMS = 60_000
+
func optionsFor(mode, upstreamMode, cdpURL, extensionPath string, launchOptions modcdp.LaunchOptions) modcdp.Options {
+ upstream := modcdp.UpstreamConfig{UpstreamMode: upstreamMode, UpstreamCDPURL: cdpURL}
+ if upstreamMode == "reversews" {
+ upstream.UpstreamReverseWSWaitTimeoutMS = reverseTransportWaitTimeoutMS
+ }
+ if upstreamMode == "nativemessaging" {
+ upstream.UpstreamNativeMessagingWaitTimeoutMS = reverseTransportWaitTimeoutMS
+ }
+ if upstreamMode == "nats" {
+ upstream.UpstreamNATSWaitTimeoutMS = reverseTransportWaitTimeoutMS
+ }
if mode == "direct" {
return modcdp.Options{
Launcher: modcdp.LauncherConfig{LauncherMode: map[bool]string{true: "remote", false: "local"}[cdpURL != ""], LauncherOptions: launchOptions},
- Upstream: modcdp.UpstreamConfig{UpstreamMode: upstreamMode, UpstreamCDPURL: cdpURL},
+ Upstream: upstream,
Injector: modcdp.InjectorConfig{InjectorMode: "auto", InjectorExtensionPath: extensionPath},
Client: modcdp.ClientConfig{ClientRoutes: clientRoutesFor(mode)},
}
@@ -43,7 +55,7 @@ func optionsFor(mode, upstreamMode, cdpURL, extensionPath string, launchOptions
}
return modcdp.Options{
Launcher: modcdp.LauncherConfig{LauncherMode: map[bool]string{true: "remote", false: "local"}[cdpURL != ""], LauncherOptions: launchOptions},
- Upstream: modcdp.UpstreamConfig{UpstreamMode: upstreamMode, UpstreamCDPURL: cdpURL},
+ Upstream: upstream,
Injector: modcdp.InjectorConfig{InjectorMode: "auto", InjectorExtensionPath: extensionPath},
Client: modcdp.ClientConfig{ClientRoutes: clientRoutesFor(mode)},
Server: server,
@@ -199,9 +211,10 @@ func main() {
headless = true
}
launchOptions = modcdp.LaunchOptions{
- ExecutablePath: chromePath,
- Headless: &headless,
- Sandbox: &sandbox,
+ ExecutablePath: chromePath,
+ ChromeReadyTimeoutMS: 60_000,
+ Headless: &headless,
+ Sandbox: &sandbox,
}
}
@@ -209,7 +222,7 @@ func main() {
var (
eventsMu sync.Mutex
targetCreatedEvents []modcdp.TargetTargetCreatedEvent
- foregroundEvents []map[string]any
+ pageTargetEvents []map[string]any
)
cdp.Target.On.TargetCreated(func(event modcdp.TargetTargetCreatedEvent) {
fmt.Printf("Target.targetCreated -> %s\n", event.TargetID())
@@ -421,34 +434,21 @@ func main() {
})
fmt.Println("Custom.demoEvent ->", demoEvent)
- foregroundEventRegistrationRaw, err := cdp.Mod.AddCustomEvent(modcdp.CustomEvent{Name: "Custom.foregroundTargetChanged"})
+ pageTargetEventRegistrationRaw, err := cdp.Mod.AddCustomEvent(modcdp.CustomEvent{Name: "Custom.pageTargetUpdated"})
if err != nil {
- log.Fatalf("Mod.addCustomEvent Custom.foregroundTargetChanged: %v", err)
+ log.Fatalf("Mod.addCustomEvent Custom.pageTargetUpdated: %v", err)
}
- foregroundEventRegistration := mustMap(foregroundEventRegistrationRaw, "Mod.addCustomEvent Custom.foregroundTargetChanged")
- if foregroundEventRegistration["registered"] != true {
- log.Fatalf("unexpected foreground event registration: %v", foregroundEventRegistration)
+ pageTargetEventRegistration := mustMap(pageTargetEventRegistrationRaw, "Mod.addCustomEvent Custom.pageTargetUpdated")
+ if pageTargetEventRegistration["registered"] != true {
+ log.Fatalf("unexpected page target event registration: %v", pageTargetEventRegistration)
}
- cdp.On("Custom.foregroundTargetChanged", func(p any) {
+ cdp.On("Custom.pageTargetUpdated", func(p any) {
event, _ := p.(map[string]any)
- fmt.Printf("Custom.foregroundTargetChanged -> %v\n", event)
+ fmt.Printf("Custom.pageTargetUpdated -> %v\n", event)
eventsMu.Lock()
- foregroundEvents = append(foregroundEvents, event)
+ pageTargetEvents = append(pageTargetEvents, event)
eventsMu.Unlock()
})
- if _, err := cdp.Mod.Evaluate(map[string]any{
- "expression": `async () => {
- chrome.tabs.onActivated.addListener(async ({ tabId }) => {
- const targets = await chrome.debugger.getTargets();
- const target = targets.find(target => target.type === "page" && target.tabId === tabId);
- const tab = await chrome.tabs.get(tabId).catch(() => null);
- await cdp.emit("Custom.foregroundTargetChanged", { tabId, targetId: target?.id ?? null, url: target?.url ?? tab?.url ?? null });
- });
- return true;
- }`,
- }); err != nil {
- log.Fatal(err)
- }
if _, err := cdp.Target.SetDiscoverTargets(modcdp.TargetSetDiscoverTargetsParams{Discover: true}); err != nil {
log.Fatal(err)
@@ -485,49 +485,67 @@ func main() {
}
fmt.Println("normal event matched ->", createdTargetID)
+ tabFromTargetRaw, err := cdp.Send("Custom.TabIdFromTargetId", map[string]any{"targetId": createdTargetID})
+ if err != nil {
+ log.Fatalf("Custom.TabIdFromTargetId: %v", err)
+ }
+ tabFromTarget, _ := tabFromTargetRaw.(map[string]any)
+ b, _ = json.Marshal(tabFromTarget)
+ fmt.Println("Custom.TabIdFromTargetId ->", string(b))
+
if _, err := cdp.Target.ActivateTarget(modcdp.TargetActivateTargetParams{TargetID: modcdp.TargetTargetID(createdTargetID)}); err != nil {
log.Fatalf("Target.activateTarget: %v", err)
}
+ pageTargetEmitRaw, err := cdp.Mod.Evaluate(map[string]any{
+ "params": map[string]any{"targetId": createdTargetID},
+ "expression": `async ({ targetId }) => {
+ const targets = await chrome.debugger.getTargets();
+ const target = targets.find(target => target.id === targetId);
+ if (!target?.id) throw new Error(` + "`target ${targetId} not found`" + `);
+ await cdp.emit("Custom.pageTargetUpdated", { targetId: target.id, url: target.url ?? null });
+ return { emitted: true, targetId: target.id };
+ }`,
+ })
+ if err != nil {
+ log.Fatalf("Custom.pageTargetUpdated emit: %v", err)
+ }
+ pageTargetEmit := mustMap(pageTargetEmitRaw, "Custom.pageTargetUpdated emit")
+ if pageTargetEmit["emitted"] != true || pageTargetEmit["targetId"] != createdTargetID {
+ log.Fatalf("unexpected Custom.pageTargetUpdated emit result: %v", pageTargetEmit)
+ }
deadline = time.Now().Add(3 * time.Second)
- var foreground map[string]any
+ var pageTarget map[string]any
for time.Now().Before(deadline) {
eventsMu.Lock()
- for _, event := range foregroundEvents {
+ for _, event := range pageTargetEvents {
if event["targetId"] == createdTargetID {
- foreground = event
+ pageTarget = event
break
}
}
eventsMu.Unlock()
- if foreground != nil {
+ if pageTarget != nil {
break
}
time.Sleep(20 * time.Millisecond)
}
- if foreground == nil {
- log.Fatalf("expected Custom.foregroundTargetChanged for %s", createdTargetID)
+ if pageTarget == nil {
+ log.Fatalf("expected Custom.pageTargetUpdated for %s", createdTargetID)
}
- tabFromTargetRaw, err := cdp.Send("Custom.TabIdFromTargetId", map[string]any{"targetId": createdTargetID})
- if err != nil {
- log.Fatalf("Custom.TabIdFromTargetId: %v", err)
- }
- tabFromTarget, _ := tabFromTargetRaw.(map[string]any)
- foregroundTabID, _ := foreground["tabId"].(float64)
+ pageTargetTabID, _ := pageTarget["tabId"].(float64)
tabID, _ := tabFromTarget["tabId"].(float64)
- if tabID != foregroundTabID {
+ if tabID != pageTargetTabID {
log.Fatalf("unexpected Custom.TabIdFromTargetId result: %v", tabFromTarget)
}
- b, _ = json.Marshal(tabFromTarget)
- fmt.Println("Custom.TabIdFromTargetId ->", string(b))
- targetFromTabRaw, err := cdp.Send("Custom.targetIdFromTabId", map[string]any{"tabId": foreground["tabId"]})
+ targetFromTabRaw, err := cdp.Send("Custom.targetIdFromTabId", map[string]any{"tabId": pageTarget["tabId"]})
if err != nil {
log.Fatalf("Custom.targetIdFromTabId: %v", err)
}
targetFromTab, _ := targetFromTabRaw.(map[string]any)
middlewareTabID, _ := targetFromTab["tabId"].(float64)
- if targetFromTab["targetId"] != createdTargetID || middlewareTabID != foregroundTabID {
+ if targetFromTab["targetId"] != createdTargetID || middlewareTabID != pageTargetTabID {
log.Fatalf("unexpected Custom.targetIdFromTabId/middleware result: %v", targetFromTab)
}
b, _ = json.Marshal(targetFromTab)
diff --git a/go/go.mod b/go/go.mod
index ad58ae9..259e5d1 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -3,7 +3,7 @@ module github.com/browserbase/modcdp/go
go 1.25.0
require (
- github.com/ArchiveBox/abxbus/abxbus-go v0.0.0-20260508073455-c5b79a4483f1
+ github.com/ArchiveBox/abxbus/abxbus-go/v2 v2.5.2
github.com/gobwas/ws v1.4.0
golang.org/x/term v0.6.0
)
diff --git a/go/go.sum b/go/go.sum
index 8591e1d..5b1f7d4 100644
--- a/go/go.sum
+++ b/go/go.sum
@@ -1,5 +1,5 @@
-github.com/ArchiveBox/abxbus/abxbus-go v0.0.0-20260508073455-c5b79a4483f1 h1:AMZ2Zr6mfP9aDuKjcJvxHkZvlqqf6jQhyk5Ju3jyfY8=
-github.com/ArchiveBox/abxbus/abxbus-go v0.0.0-20260508073455-c5b79a4483f1/go.mod h1:yYB8LZqGpYryBo38Hl2s3dhPZweuUwqQxiMZGrY8XXE=
+github.com/ArchiveBox/abxbus/abxbus-go/v2 v2.5.2 h1:qpCQCuZ/Nfi6mHf00Ed9nI88JBaAMOh+PhXEO1hdcfQ=
+github.com/ArchiveBox/abxbus/abxbus-go/v2 v2.5.2/go.mod h1:Nk7W3WwqKgQw07gQ6V+UdaUJzJwa0Qj78F/n7sERs6A=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
diff --git a/go/modcdp/client/ModCDPClient.go b/go/modcdp/client/ModCDPClient.go
index ed0346e..d3e32a4 100644
--- a/go/modcdp/client/ModCDPClient.go
+++ b/go/modcdp/client/ModCDPClient.go
@@ -23,14 +23,13 @@ import (
"net"
"net/http"
"net/url"
- "os"
"reflect"
"regexp"
"strings"
"sync"
"time"
- abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/jsonschema"
+ abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema"
"github.com/browserbase/modcdp/go/modcdp/injector"
"github.com/browserbase/modcdp/go/modcdp/launcher"
"github.com/browserbase/modcdp/go/modcdp/router"
@@ -110,7 +109,6 @@ var NewAutoSessionRouter = router.NewAutoSessionRouter
var DefaultModCDPServiceWorkerURLSuffixes = injector.DefaultModCDPServiceWorkerURLSuffixes
-const DefaultModCDPWakePath = injector.DefaultModCDPWakePath
const DefaultModCDPExtensionID = injector.DefaultModCDPExtensionID
const DefaultUpstreamReverseWSBind = transportpkg.DefaultUpstreamReverseWSBind
const DefaultUpstreamReverseWSWaitTimeoutMS = transportpkg.DefaultUpstreamReverseWSWaitTimeoutMS
@@ -238,8 +236,6 @@ type InjectorConfig struct {
InjectorMode string `json:"injector_mode,omitempty"`
InjectorExtensionPath string `json:"injector_extension_path,omitempty"`
InjectorExtensionID string `json:"injector_extension_id,omitempty"`
- InjectorWakePath string `json:"injector_wake_path,omitempty"`
- InjectorWakeURL string `json:"injector_wake_url,omitempty"`
InjectorServiceWorkerURLIncludes []string `json:"injector_service_worker_url_includes,omitempty"`
InjectorServiceWorkerURLSuffixes []string `json:"injector_service_worker_url_suffixes,omitempty"`
InjectorTrustServiceWorkerTarget bool `json:"injector_trust_service_worker_target,omitempty"`
@@ -563,6 +559,7 @@ func New(opts Options) *ModCDPClient {
if *client.Client.ClientHydrateAliases {
initCDPSurface(client)
}
+ client.hydrateNativeProtocolSchemas()
client.hydrateCustomSurface()
return client
}
@@ -929,11 +926,6 @@ func (c *ModCDPClient) serverConfigureParams(customCommands []map[string]any, cu
if c.Upstream.UpstreamNATSSubjectPrefix != "" {
upstream["upstream_nats_subject_prefix"] = c.Upstream.UpstreamNATSSubjectPrefix
}
- if c.transport != nil {
- if reverseWSURL := c.transport.GetInjectorConfig().UpstreamReverseWSURL; reverseWSURL != "" {
- upstream["upstream_reversews_url"] = reverseWSURL
- }
- }
return map[string]any{
"upstream": upstream,
"client": map[string]any{
@@ -972,6 +964,40 @@ func cloneSchema(schema map[string]any) map[string]any {
return normalized
}
+func nativeResultSchema(schema map[string]any) map[string]any {
+ normalized := cloneSchema(schema)
+ allowNativeResultExtensions(normalized)
+ return normalized
+}
+
+func allowNativeResultExtensions(schema map[string]any) {
+ if schema == nil {
+ return
+ }
+ if schemaType, _ := schema["type"].(string); schemaType == "object" {
+ schema["additionalProperties"] = true
+ if properties, ok := schema["properties"].(map[string]any); ok {
+ for _, property := range properties {
+ if propertySchema, ok := property.(map[string]any); ok {
+ allowNativeResultExtensions(propertySchema)
+ }
+ }
+ }
+ }
+ if items, ok := schema["items"].(map[string]any); ok {
+ allowNativeResultExtensions(items)
+ }
+ for _, key := range []string{"anyOf", "oneOf", "allOf"} {
+ if schemas, ok := schema[key].([]any); ok {
+ for _, entry := range schemas {
+ if entrySchema, ok := entry.(map[string]any); ok {
+ allowNativeResultExtensions(entrySchema)
+ }
+ }
+ }
+ }
+}
+
func resultUnwrapKeyFromSchema(schema map[string]any) string {
properties, _ := schema["properties"].(map[string]any)
if len(properties) != 1 {
@@ -1146,8 +1172,7 @@ func (c *ModCDPClient) validateEventData(event string, data any) (any, bool) {
return data, true
}
if err := abxjsonschema.Validate(schema, data); err != nil {
- fmt.Fprintf(os.Stderr, "[ModCDPClient] %s event did not match event_schema: %v\n", event, err)
- return nil, false
+ panic(fmt.Errorf("%s event did not match event_schema: %w", event, err))
}
return data, true
}
@@ -1444,7 +1469,8 @@ func (c *ModCDPClient) extensionInjectorsForConfig() []extensionInjector {
return nil
}
var injectors []extensionInjector
- if c.Injector.InjectorMode == "auto" || c.Injector.InjectorMode == "discover" {
+ preferLaunchInjection := c.Injector.InjectorMode == "auto" && c.Launcher.LauncherMode == "local"
+ if (c.Injector.InjectorMode == "auto" || c.Injector.InjectorMode == "discover") && !preferLaunchInjection {
injector := NewDiscoveredExtensionInjector(ExtensionInjectorConfig{})
injectors = append(injectors, &injector)
}
@@ -1460,6 +1486,10 @@ func (c *ModCDPClient) extensionInjectorsForConfig() []extensionInjector {
injector := NewExtensionsLoadUnpackedInjector(ExtensionInjectorConfig{})
injectors = append(injectors, &injector)
}
+ if preferLaunchInjection {
+ injector := NewDiscoveredExtensionInjector(ExtensionInjectorConfig{})
+ injectors = append(injectors, &injector)
+ }
if c.Injector.InjectorMode == "auto" || c.Injector.InjectorMode == "borrow" {
injector := NewBorrowedExtensionInjector(ExtensionInjectorConfig{})
injectors = append(injectors, &injector)
@@ -1497,8 +1527,6 @@ func (c *ModCDPClient) baseExtensionInjectorConfig(send SendCDP) ExtensionInject
},
InjectorExtensionPath: c.Injector.InjectorExtensionPath,
InjectorExtensionID: c.Injector.InjectorExtensionID,
- InjectorWakePath: firstNonEmptyString(c.Injector.InjectorWakePath, DefaultModCDPWakePath),
- InjectorWakeURL: c.Injector.InjectorWakeURL,
InjectorServiceWorkerURLIncludes: c.Injector.InjectorServiceWorkerURLIncludes,
InjectorServiceWorkerURLSuffixes: c.Injector.InjectorServiceWorkerURLSuffixes,
InjectorTrustServiceWorkerTarget: trustMatchedServiceWorker,
diff --git a/go/modcdp/client/ModCDPClientCustomFlatNamespace_test.go b/go/modcdp/client/ModCDPClientCustomFlatNamespace_test.go
index 24a540a..76fc943 100644
--- a/go/modcdp/client/ModCDPClientCustomFlatNamespace_test.go
+++ b/go/modcdp/client/ModCDPClientCustomFlatNamespace_test.go
@@ -5,7 +5,7 @@ import (
"testing"
"time"
- abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/jsonschema"
+ abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema"
)
func TestCustomCommandsInstallFlatNamespaceThroughRealServiceWorker(t *testing.T) {
@@ -24,7 +24,6 @@ func TestCustomCommandsInstallFlatNamespaceThroughRealServiceWorker(t *testing.T
Launcher: LauncherConfig{LauncherMode: "local",
LauncherOptions: LaunchOptions{
Headless: boolPtr(true),
- Sandbox: boolPtr(false),
},
},
Upstream: UpstreamConfig{UpstreamMode: "ws"},
@@ -84,7 +83,6 @@ func TestCustomEventsValidateRawStringHandlersThroughRealServiceWorker(t *testin
Launcher: LauncherConfig{LauncherMode: "local",
LauncherOptions: LaunchOptions{
Headless: boolPtr(true),
- Sandbox: boolPtr(false),
},
},
Upstream: UpstreamConfig{UpstreamMode: "ws"},
@@ -236,7 +234,15 @@ func TestTypedCustomEventRegistrationAndHandler(t *testing.T) {
if got := <-seen; got != "ok" {
t.Fatalf("unexpected typed event data %q", got)
}
- if _, ok := cdp.validateEventData("Custom.someEvent", map[string]any{"data": 123}); ok {
- t.Fatal("expected typed event schema to reject wrong data type")
- }
+ expectPanic(t, func() { cdp.validateEventData("Custom.someEvent", map[string]any{"data": 123}) })
+}
+
+func expectPanic(t *testing.T, fn func()) {
+ t.Helper()
+ defer func() {
+ if recover() == nil {
+ t.Fatal("expected panic")
+ }
+ }()
+ fn()
}
diff --git a/go/modcdp/client/ModCDPClientRoutedDefaultOverrides_test.go b/go/modcdp/client/ModCDPClientRoutedDefaultOverrides_test.go
index 604911a..f91cb10 100644
--- a/go/modcdp/client/ModCDPClientRoutedDefaultOverrides_test.go
+++ b/go/modcdp/client/ModCDPClientRoutedDefaultOverrides_test.go
@@ -60,25 +60,31 @@ async (payload, next) => {
func TestModCDPClientRoutedDefaultOverrides(t *testing.T) {
headless := true
- sandbox := false
extensionPath, err := filepath.Abs("../../../dist/extension")
if err != nil {
t.Fatal(err)
}
- chrome, err := NewLocalBrowserLauncher(LaunchOptions{
- Headless: &headless,
- Sandbox: &sandbox,
- ExtraArgs: []string{"--load-extension=" + extensionPath},
- }).Launch(LaunchOptions{})
- if err != nil {
+ owner := New(Options{
+ Launcher: LauncherConfig{
+ LauncherMode: "local",
+ LauncherOptions: LaunchOptions{Headless: &headless},
+ },
+ Upstream: UpstreamConfig{UpstreamMode: "ws"},
+ Injector: InjectorConfig{
+ InjectorMode: "auto",
+ InjectorExtensionPath: extensionPath,
+ InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
+ InjectorTrustServiceWorkerTarget: true,
+ },
+ })
+ if err := owner.Connect(); err != nil {
t.Fatal(err)
}
cdp := New(Options{
Launcher: LauncherConfig{LauncherMode: "remote"},
- Upstream: UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL},
+ Upstream: UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: owner.CDPURL},
Injector: InjectorConfig{
- InjectorMode: "auto",
- InjectorExtensionPath: extensionPath,
+ InjectorMode: "discover",
InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
InjectorTrustServiceWorkerTarget: true,
},
@@ -90,24 +96,24 @@ func TestModCDPClientRoutedDefaultOverrides(t *testing.T) {
},
},
Server: &ServerConfig{
- ServerLoopbackCDPURL: chrome.CDPURL,
+ ServerLoopbackCDPURL: owner.CDPURL,
ServerRoutes: map[string]string{"*.*": "loopback_cdp"},
},
})
- defer chrome.Close()
+ defer owner.Close()
defer cdp.Close()
if err := cdp.Connect(); err != nil {
t.Fatal(err)
}
- if cdp.CDPURL != chrome.CDPURL {
- t.Fatalf("CDPURL = %q, expected %q", cdp.CDPURL, chrome.CDPURL)
+ if cdp.CDPURL != owner.CDPURL {
+ t.Fatalf("CDPURL = %q, expected %q", cdp.CDPURL, owner.CDPURL)
}
- if cdp.Server.ServerLoopbackCDPURL != chrome.CDPURL {
- t.Fatalf("ServerLoopbackCDPURL = %q, expected %q", cdp.Server.ServerLoopbackCDPURL, chrome.CDPURL)
+ if cdp.Server.ServerLoopbackCDPURL != owner.CDPURL {
+ t.Fatalf("ServerLoopbackCDPURL = %q, expected %q", cdp.Server.ServerLoopbackCDPURL, owner.CDPURL)
}
- rawTargets, err := cdp.sendMessage("Target.getTargets", map[string]any{}, "")
+ rawTargets, err := cdp.Send("Target.getTargets", nil)
if err != nil {
t.Fatal(err)
}
diff --git a/go/modcdp/client/ModCDPClient_protocol_validation_test.go b/go/modcdp/client/ModCDPClient_protocol_validation_test.go
new file mode 100644
index 0000000..97d0f69
--- /dev/null
+++ b/go/modcdp/client/ModCDPClient_protocol_validation_test.go
@@ -0,0 +1,145 @@
+package client
+
+import (
+ "testing"
+
+ abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema"
+)
+
+type protocolValidationCustomEchoParams struct {
+ Text string `json:"text"`
+}
+
+type protocolValidationCustomEchoResult struct {
+ OK bool `json:"ok"`
+}
+
+type protocolValidationCustomReadyEvent struct {
+ OK bool `json:"ok"`
+}
+
+func TestProtocolValidationCoversNativeMethodsNativeEventsCustomMethodsCustomEventsAndNativeOverrides(t *testing.T) {
+ cdp := New(Options{})
+
+ runtimeParams := RuntimeEvaluateParams{Expression: "1 + 1", ReturnByValue: Bool(true)}
+ runtimeResult := RuntimeEvaluateResult{Result: RuntimeRemoteObject{Type: "number", Value: 2}}
+ nativeEvent := TargetTargetCreatedEvent{
+ TargetInfo: TargetTargetInfo{
+ TargetID: TargetTargetID("target-1"),
+ Type: "page",
+ Title: "Example",
+ URL: "https://example.com",
+ Attached: false,
+ CanAccessOpener: false,
+ },
+ }
+ customParams := protocolValidationCustomEchoParams{Text: "ok"}
+ customResult := protocolValidationCustomEchoResult{OK: true}
+ customEvent := protocolValidationCustomReadyEvent{OK: true}
+
+ if err := cdp.validateCommandParams("Runtime.evaluate", mustParamsMap(t, runtimeParams)); err != nil {
+ t.Fatalf("native Runtime.evaluate params should validate: %v", err)
+ }
+ if err := cdp.validateCommandResult("Runtime.evaluate", runtimeResult); err != nil {
+ t.Fatalf("native Runtime.evaluate result should validate: %v", err)
+ }
+ if _, ok := cdp.validateEventData("Target.targetCreated", nativeEvent); !ok {
+ t.Fatal("native Target.targetCreated event should validate")
+ }
+ if err := cdp.validateCommandParams("Runtime.evaluate", map[string]any{}); err == nil {
+ t.Fatal("expected Runtime.evaluate params validation to reject missing expression")
+ }
+ if err := cdp.validateCommandResult("Runtime.evaluate", map[string]any{}); err == nil {
+ t.Fatal("expected Runtime.evaluate result validation to reject missing result")
+ }
+ expectPanic(t, func() { cdp.validateEventData("Target.targetCreated", map[string]any{}) })
+
+ if _, err := cdp.Mod.AddCustomCommand(CustomCommand{
+ Name: "Custom.echo",
+ ParamsSchema: abxjsonschema.SchemaFor[protocolValidationCustomEchoParams](),
+ ResultSchema: abxjsonschema.SchemaFor[protocolValidationCustomEchoResult](),
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := cdp.Mod.AddCustomEvent(CustomEvent{
+ Name: "Custom.ready",
+ EventSchema: abxjsonschema.SchemaFor[protocolValidationCustomReadyEvent](),
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := cdp.validateCommandParams("Custom.echo", mustParamsMap(t, customParams)); err != nil {
+ t.Fatalf("custom params should validate: %v", err)
+ }
+ if err := cdp.validateCommandResult("Custom.echo", customResult); err != nil {
+ t.Fatalf("custom result should validate: %v", err)
+ }
+ if _, ok := cdp.validateEventData("Custom.ready", customEvent); !ok {
+ t.Fatal("custom event should validate")
+ }
+ if err := cdp.validateCommandParams("Custom.echo", map[string]any{"text": 1}); err == nil {
+ t.Fatal("expected custom params validation to reject wrong text type")
+ }
+ if err := cdp.validateCommandResult("Custom.echo", map[string]any{"ok": "yes"}); err == nil {
+ t.Fatal("expected custom result validation to reject wrong ok type")
+ }
+ expectPanic(t, func() { cdp.validateEventData("Custom.ready", map[string]any{"ok": "yes"}) })
+
+ if _, err := cdp.Mod.AddCustomCommand(CustomCommand{
+ Name: "Target.getTargets",
+ ResultSchema: map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "targetInfos": map[string]any{
+ "type": "array",
+ "items": map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "targetId": map[string]any{"type": "string"},
+ "type": map[string]any{"type": "string"},
+ "title": map[string]any{"type": "string"},
+ "url": map[string]any{"type": "string"},
+ "attached": map[string]any{"type": "boolean"},
+ "canAccessOpener": map[string]any{"type": "boolean"},
+ "tabId": map[string]any{"type": "integer"},
+ },
+ "required": []any{"targetId", "type", "title", "url", "attached", "canAccessOpener"},
+ "additionalProperties": true,
+ },
+ },
+ },
+ "required": []any{"targetInfos"},
+ "additionalProperties": true,
+ },
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := cdp.Mod.AddCustomEvent(CustomEvent{Name: "Target.targetCreated"}); err != nil {
+ t.Fatal(err)
+ }
+
+ extendedTargetInfo := map[string]any{
+ "targetId": "target-1",
+ "type": "page",
+ "title": "Example",
+ "url": "https://example.com",
+ "attached": false,
+ "canAccessOpener": false,
+ "tabId": 7,
+ }
+ if err := cdp.validateCommandResult("Target.getTargets", map[string]any{"targetInfos": []any{extendedTargetInfo}}); err != nil {
+ t.Fatalf("extended native command result should validate: %v", err)
+ }
+ if _, ok := cdp.validateEventData("Target.targetCreated", map[string]any{"targetInfo": extendedTargetInfo}); !ok {
+ t.Fatal("extended native event should validate")
+ }
+}
+
+func mustParamsMap(t *testing.T, value any) map[string]any {
+ t.Helper()
+ params, err := cdpParamsMap(value)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return params
+}
diff --git a/go/modcdp/client/ModCDPClient_test.go b/go/modcdp/client/ModCDPClient_test.go
index a640044..7dad078 100644
--- a/go/modcdp/client/ModCDPClient_test.go
+++ b/go/modcdp/client/ModCDPClient_test.go
@@ -3,9 +3,14 @@ package client
import (
"context"
"encoding/json"
+ "fmt"
"os"
"path/filepath"
+ "reflect"
+ "regexp"
"runtime"
+ "sort"
+ "strconv"
"strings"
"testing"
"time"
@@ -167,7 +172,14 @@ func TestModCDPClientDispatchesRootEventsBeforeExtensionSessionAttached(t *testi
cdp.handleEventMessage(map[string]any{
"method": "Target.targetCreated",
"params": map[string]any{
- "targetInfo": map[string]any{"targetId": "target-1", "type": "page", "url": "about:blank"},
+ "targetInfo": map[string]any{
+ "targetId": "target-1",
+ "type": "page",
+ "title": "about:blank",
+ "url": "about:blank",
+ "attached": false,
+ "canAccessOpener": false,
+ },
},
})
@@ -195,7 +207,14 @@ func TestModCDPClientEventDispatchSnapshotsHandlersWhenOnceRemovesItself(t *test
cdp.handleEventMessage(map[string]any{
"method": "Target.targetCreated",
"params": map[string]any{
- "targetInfo": map[string]any{"targetId": "target-1", "type": "page", "url": "about:blank"},
+ "targetInfo": map[string]any{
+ "targetId": "target-1",
+ "type": "page",
+ "title": "about:blank",
+ "url": "about:blank",
+ "attached": false,
+ "canAccessOpener": false,
+ },
},
})
@@ -215,7 +234,14 @@ func TestModCDPClientEventDispatchSnapshotsHandlersWhenOnceRemovesItself(t *test
cdp.handleEventMessage(map[string]any{
"method": "Target.targetCreated",
"params": map[string]any{
- "targetInfo": map[string]any{"targetId": "target-2", "type": "page", "url": "about:blank"},
+ "targetInfo": map[string]any{
+ "targetId": "target-2",
+ "type": "page",
+ "title": "about:blank",
+ "url": "about:blank",
+ "attached": false,
+ "canAccessOpener": false,
+ },
},
})
@@ -430,6 +456,38 @@ func TestModCDPClientDefaultsLaunchedModCDPServerUpstreamsToExtensionAuto(t *tes
}
}
+func TestModCDPClientOrdersLocalAutoInjectionAsLaunchFlagThenLoadUnpackedFallback(t *testing.T) {
+ cdp := New(Options{
+ Launcher: LauncherConfig{LauncherMode: "local"},
+ Injector: InjectorConfig{InjectorMode: "auto"},
+ })
+
+ got := []string{}
+ for _, injector := range cdp.extensionInjectorsForConfig() {
+ switch injector.(type) {
+ case *LocalBrowserLaunchExtensionInjector:
+ got = append(got, "LocalBrowserLaunchExtensionInjector")
+ case *ExtensionsLoadUnpackedInjector:
+ got = append(got, "ExtensionsLoadUnpackedInjector")
+ case *DiscoveredExtensionInjector:
+ got = append(got, "DiscoveredExtensionInjector")
+ case *BorrowedExtensionInjector:
+ got = append(got, "BorrowedExtensionInjector")
+ default:
+ got = append(got, fmt.Sprintf("%T", injector))
+ }
+ }
+ want := []string{
+ "LocalBrowserLaunchExtensionInjector",
+ "ExtensionsLoadUnpackedInjector",
+ "DiscoveredExtensionInjector",
+ "BorrowedExtensionInjector",
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("injector order = %#v", got)
+ }
+}
+
func TestModCDPClientRejectsUnknownComponentModesAtTheirOwningFactoryBoundary(t *testing.T) {
cases := []struct {
name string
@@ -489,17 +547,21 @@ func TestModCDPClientOnlyExposesInjectorAttachAfterCDPSendIsAvailable(t *testing
func TestModCDPClientConnectsWithLocalLaunchAndInjectorChain(t *testing.T) {
headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == ""
- sandbox := runtime.GOOS != "linux"
+ extensionPath, err := filepath.Abs("../../../dist/extension")
+ if err != nil {
+ t.Fatal(err)
+ }
cdp := New(Options{
Launcher: LauncherConfig{LauncherMode: "local",
LauncherOptions: LaunchOptions{
- Headless: boolPtr(headless),
- Sandbox: boolPtr(sandbox),
+ Headless: boolPtr(headless),
+ ChromeReadyTimeoutMS: 60_000,
},
},
Upstream: UpstreamConfig{UpstreamMode: "ws"},
Injector: InjectorConfig{
- InjectorMode: "inject",
+ InjectorMode: "auto",
+ InjectorExtensionPath: extensionPath,
InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
InjectorTrustServiceWorkerTarget: true,
InjectorServiceWorkerProbeTimeoutMS: 30_000,
@@ -518,7 +580,9 @@ func TestModCDPClientConnectsWithLocalLaunchAndInjectorChain(t *testing.T) {
if err := cdp.Connect(); err != nil {
t.Fatal(err)
}
- if cdp.ConnectTiming["injector_source"] != "local_launch" {
+ switch cdp.ConnectTiming["injector_source"] {
+ case "discovered", "local_launch", "extensions_load_unpacked", "borrowed":
+ default:
t.Fatalf("injector_source = %v", cdp.ConnectTiming["injector_source"])
}
if cdp.ExtensionID != DefaultModCDPExtensionID {
@@ -533,6 +597,24 @@ func TestModCDPClientConnectsWithLocalLaunchAndInjectorChain(t *testing.T) {
if result != "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js" {
t.Fatalf("Mod.evaluate = %#v", result)
}
+ contextsRaw, err := cdp.Mod.Evaluate(map[string]any{
+ "expression": "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => ({ type: context.contextType, url: context.documentUrl || context.origin || '' })))",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ contexts, _ := contextsRaw.([]any)
+ foundOffscreen := false
+ for _, rawContext := range contexts {
+ context, _ := rawContext.(map[string]any)
+ if context["type"] == "OFFSCREEN_DOCUMENT" &&
+ context["url"] == "chrome-extension://"+DefaultModCDPExtensionID+"/offscreen/keepalive.html" {
+ foundOffscreen = true
+ }
+ }
+ if !foundOffscreen {
+ t.Fatalf("expected offscreen keepalive context, got %#v", contextsRaw)
+ }
directTargetRaw, err := cdp.Send("Target.createTarget", map[string]any{"url": "about:blank#direct-session-routing"})
if err != nil {
t.Fatal(err)
@@ -624,15 +706,17 @@ func TestModCDPClientConnectsWithLocalLaunchAndInjectorChain(t *testing.T) {
func TestModCDPClientCloseDoesNotCloseRemoteBrowserItDidNotLaunch(t *testing.T) {
headless := true
- sandbox := false
extensionPath, err := filepath.Abs("../../../dist/extension")
if err != nil {
t.Fatal(err)
}
chrome, err := NewLocalBrowserLauncher(LaunchOptions{
- Headless: &headless,
- Sandbox: &sandbox,
- ExtraArgs: []string{"--load-extension=" + extensionPath},
+ Headless: &headless,
+ ChromeReadyTimeoutMS: 60_000,
+ // This test manually supplies --load-extension, so it intentionally uses
+ // the launch-flag browser path instead of relying on the client fallback.
+ ExecutablePath: reverseWSTestBrowserPath(t),
+ ExtraArgs: []string{"--load-extension=" + extensionPath},
}).Launch(LaunchOptions{})
if err != nil {
t.Fatal(err)
@@ -646,15 +730,18 @@ func TestModCDPClientCloseDoesNotCloseRemoteBrowserItDidNotLaunch(t *testing.T)
t.Fatal(err)
}
defer rawConn.Close()
-
cdp := New(Options{
Launcher: LauncherConfig{LauncherMode: "remote"},
Upstream: UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL},
Injector: InjectorConfig{
- InjectorMode: "discover",
- InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
- InjectorTrustServiceWorkerTarget: true,
+ InjectorMode: "auto",
+ InjectorExtensionPath: extensionPath,
+ InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
+ InjectorTrustServiceWorkerTarget: true,
+ InjectorServiceWorkerReadyTimeoutMS: 30_000,
+ InjectorServiceWorkerProbeTimeoutMS: 30_000,
},
+ Client: ClientConfig{ClientRoutes: map[string]string{"*.*": "direct_cdp"}},
})
if err := cdp.Connect(); err != nil {
t.Fatal(err)
@@ -695,12 +782,14 @@ func TestModCDPClientCloseKeepsInjectorFilesUntilAfterLaunchedBrowserShutdown(t
Launcher: LauncherConfig{LauncherMode: "local",
LauncherOptions: LaunchOptions{
Headless: boolPtr(true),
- Sandbox: boolPtr(false),
+ // After explicit CHROME_PATH and CI /usr/bin/chromium, this test uses
+ // Chrome for Testing because Canary rejects --load-extension in this
+ // local launch injector path.
+ ExecutablePath: reverseWSTestBrowserPath(t),
},
},
Upstream: UpstreamConfig{
- UpstreamMode: "reversews",
- UpstreamReverseWSWaitTimeoutMS: 30_000,
+ UpstreamMode: "ws",
},
Injector: InjectorConfig{
InjectorMode: "auto",
@@ -715,16 +804,16 @@ func TestModCDPClientCloseKeepsInjectorFilesUntilAfterLaunchedBrowserShutdown(t
if err := cdp.Connect(); err != nil {
t.Fatal(err)
}
- var localInjector *LocalBrowserLaunchExtensionInjector
+ var localLaunchInjector *LocalBrowserLaunchExtensionInjector
for _, injector := range cdp.extensionInjectors {
if typed, ok := injector.(*LocalBrowserLaunchExtensionInjector); ok {
- localInjector = typed
+ localLaunchInjector = typed
}
}
- if localInjector == nil {
+ if localLaunchInjector == nil {
t.Fatal("expected LocalBrowserLaunchExtensionInjector")
}
- unpackedExtensionPath := localInjector.UnpackedExtensionPath
+ unpackedExtensionPath := localLaunchInjector.UnpackedExtensionPath
if unpackedExtensionPath == extensionPath {
t.Fatalf("UnpackedExtensionPath = %q", unpackedExtensionPath)
}
@@ -760,7 +849,6 @@ func TestModCDPClientCloseClearsTopLevelConnectionState(t *testing.T) {
Launcher: LauncherConfig{LauncherMode: "local",
LauncherOptions: LaunchOptions{
Headless: boolPtr(true),
- Sandbox: boolPtr(false),
},
},
Upstream: UpstreamConfig{UpstreamMode: "ws"},
@@ -789,6 +877,108 @@ func TestModCDPClientCloseClearsTopLevelConnectionState(t *testing.T) {
}
}
+func reverseWSTestBrowserPath(t *testing.T) string {
+ t.Helper()
+ explicitCandidates := []string{os.Getenv("CHROME_PATH")}
+ if runtime.GOOS == "linux" {
+ explicitCandidates = append(explicitCandidates, "/usr/bin/chromium")
+ }
+ for _, candidate := range explicitCandidates {
+ if candidate == "" {
+ continue
+ }
+ if _, err := os.Stat(candidate); err == nil {
+ return candidate
+ }
+ }
+ home, err := os.UserHomeDir()
+ if err != nil || home == "" {
+ home = "."
+ }
+ localAppData := os.Getenv("LOCALAPPDATA")
+ if localAppData == "" {
+ localAppData = filepath.Join(home, "AppData", "Local")
+ }
+ var patterns []string
+ switch runtime.GOOS {
+ case "darwin":
+ patterns = []string{
+ filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"),
+ filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Chromium.app", "Contents", "MacOS", "Chromium"),
+ filepath.Join(home, "Library", "Caches", "puppeteer", "chrome", "mac*-*", "chrome-mac*", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"),
+ }
+ case "windows":
+ patterns = []string{
+ filepath.Join(localAppData, "ms-playwright", "chromium-*", "chrome-win*", "chrome.exe"),
+ filepath.Join(home, ".cache", "puppeteer", "chrome", "win*-*", "chrome.exe"),
+ }
+ default:
+ patterns = []string{
+ filepath.Join(home, ".cache", "ms-playwright", "chromium-*", "chrome-linux*", "chrome"),
+ filepath.Join("/opt", "pw-browsers", "chromium-*", "chrome-linux*", "chrome"),
+ filepath.Join(home, ".cache", "puppeteer", "chrome", "linux-*", "chrome-linux*", "chrome"),
+ }
+ }
+ var candidates []string
+ for _, pattern := range patterns {
+ matches, err := filepath.Glob(pattern)
+ if err != nil {
+ continue
+ }
+ candidates = append(candidates, matches...)
+ }
+ candidates = newestChromeForTestingFirst(candidates)
+ if len(candidates) > 0 {
+ return candidates[0]
+ }
+ t.Fatal("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.")
+ return ""
+}
+
+func newestChromeForTestingFirst(candidates []string) []string {
+ seen := map[string]bool{}
+ deduped := make([]string, 0, len(candidates))
+ for _, candidate := range candidates {
+ if candidate == "" || seen[candidate] {
+ continue
+ }
+ seen[candidate] = true
+ deduped = append(deduped, candidate)
+ }
+ sort.SliceStable(deduped, func(i, j int) bool {
+ leftVersion := maxPathNumber(deduped[i])
+ rightVersion := maxPathNumber(deduped[j])
+ if leftVersion != rightVersion {
+ return leftVersion > rightVersion
+ }
+ leftStat, leftErr := os.Stat(deduped[i])
+ rightStat, rightErr := os.Stat(deduped[j])
+ var leftMtime, rightMtime time.Time
+ if leftErr == nil {
+ leftMtime = leftStat.ModTime()
+ }
+ if rightErr == nil {
+ rightMtime = rightStat.ModTime()
+ }
+ if !leftMtime.Equal(rightMtime) {
+ return leftMtime.After(rightMtime)
+ }
+ return deduped[i] < deduped[j]
+ })
+ return deduped
+}
+
+func maxPathNumber(value string) int {
+ maxValue := 0
+ for _, raw := range regexp.MustCompile(`\d+`).FindAllString(value, -1) {
+ number, err := strconv.Atoi(raw)
+ if err == nil && number > maxValue {
+ maxValue = number
+ }
+ }
+ return maxValue
+}
+
func TestCustomCommandSchemasValidateParamsAndResults(t *testing.T) {
cdp := New(Options{
CustomCommands: []CustomCommand{
@@ -842,9 +1032,7 @@ func TestCustomEventSchemasValidatePayloads(t *testing.T) {
if _, ok := cdp.validateEventData("Custom.changed", map[string]any{"targetId": "target-1"}); !ok {
t.Fatal("expected valid event payload")
}
- if _, ok := cdp.validateEventData("Custom.changed", map[string]any{"targetId": 1}); ok {
- t.Fatal("expected invalid event payload")
- }
+ expectPanic(t, func() { cdp.validateEventData("Custom.changed", map[string]any{"targetId": 1}) })
}
func TestTypedCDPSurfaceInitializesAndEncodesParams(t *testing.T) {
diff --git a/go/modcdp/client/ModCDPPayloadSchemaNormalization_test.go b/go/modcdp/client/ModCDPPayloadSchemaNormalization_test.go
index b039776..b49102c 100644
--- a/go/modcdp/client/ModCDPPayloadSchemaNormalization_test.go
+++ b/go/modcdp/client/ModCDPPayloadSchemaNormalization_test.go
@@ -4,7 +4,7 @@ import (
"strings"
"testing"
- abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/jsonschema"
+ abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema"
)
func TestPayloadSchemaNormalizationAcceptsEmptyJSONSchemaObjects(t *testing.T) {
diff --git a/go/modcdp/client/generated_protocol_schemas.go b/go/modcdp/client/generated_protocol_schemas.go
new file mode 100644
index 0000000..507ff76
--- /dev/null
+++ b/go/modcdp/client/generated_protocol_schemas.go
@@ -0,0 +1,1576 @@
+// Code generated from generated_domains.go. DO NOT EDIT BY HAND.
+package client
+
+import abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema"
+
+func (c *ModCDPClient) hydrateNativeProtocolSchemas() {
+ c.schemaMu.Lock()
+ defer c.schemaMu.Unlock()
+ c.commandParamsSchemas["Accessibility.disable"] = abxjsonschema.SchemaFor[AccessibilityDisableParams]()
+ c.commandResultSchemas["Accessibility.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityDisableResult]())
+ c.commandParamsSchemas["Accessibility.enable"] = abxjsonschema.SchemaFor[AccessibilityEnableParams]()
+ c.commandResultSchemas["Accessibility.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityEnableResult]())
+ c.commandParamsSchemas["Accessibility.getPartialAXTree"] = abxjsonschema.SchemaFor[AccessibilityGetPartialAXTreeParams]()
+ c.commandResultSchemas["Accessibility.getPartialAXTree"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetPartialAXTreeResult]())
+ c.commandParamsSchemas["Accessibility.getFullAXTree"] = abxjsonschema.SchemaFor[AccessibilityGetFullAXTreeParams]()
+ c.commandResultSchemas["Accessibility.getFullAXTree"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetFullAXTreeResult]())
+ c.commandParamsSchemas["Accessibility.getRootAXNode"] = abxjsonschema.SchemaFor[AccessibilityGetRootAXNodeParams]()
+ c.commandResultSchemas["Accessibility.getRootAXNode"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetRootAXNodeResult]())
+ c.commandParamsSchemas["Accessibility.getAXNodeAndAncestors"] = abxjsonschema.SchemaFor[AccessibilityGetAXNodeAndAncestorsParams]()
+ c.commandResultSchemas["Accessibility.getAXNodeAndAncestors"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetAXNodeAndAncestorsResult]())
+ c.commandParamsSchemas["Accessibility.getChildAXNodes"] = abxjsonschema.SchemaFor[AccessibilityGetChildAXNodesParams]()
+ c.commandResultSchemas["Accessibility.getChildAXNodes"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetChildAXNodesResult]())
+ c.commandParamsSchemas["Accessibility.queryAXTree"] = abxjsonschema.SchemaFor[AccessibilityQueryAXTreeParams]()
+ c.commandResultSchemas["Accessibility.queryAXTree"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityQueryAXTreeResult]())
+ c.commandParamsSchemas["Animation.disable"] = abxjsonschema.SchemaFor[AnimationDisableParams]()
+ c.commandResultSchemas["Animation.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationDisableResult]())
+ c.commandParamsSchemas["Animation.enable"] = abxjsonschema.SchemaFor[AnimationEnableParams]()
+ c.commandResultSchemas["Animation.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationEnableResult]())
+ c.commandParamsSchemas["Animation.getCurrentTime"] = abxjsonschema.SchemaFor[AnimationGetCurrentTimeParams]()
+ c.commandResultSchemas["Animation.getCurrentTime"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationGetCurrentTimeResult]())
+ c.commandParamsSchemas["Animation.getPlaybackRate"] = abxjsonschema.SchemaFor[AnimationGetPlaybackRateParams]()
+ c.commandResultSchemas["Animation.getPlaybackRate"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationGetPlaybackRateResult]())
+ c.commandParamsSchemas["Animation.releaseAnimations"] = abxjsonschema.SchemaFor[AnimationReleaseAnimationsParams]()
+ c.commandResultSchemas["Animation.releaseAnimations"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationReleaseAnimationsResult]())
+ c.commandParamsSchemas["Animation.resolveAnimation"] = abxjsonschema.SchemaFor[AnimationResolveAnimationParams]()
+ c.commandResultSchemas["Animation.resolveAnimation"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationResolveAnimationResult]())
+ c.commandParamsSchemas["Animation.seekAnimations"] = abxjsonschema.SchemaFor[AnimationSeekAnimationsParams]()
+ c.commandResultSchemas["Animation.seekAnimations"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationSeekAnimationsResult]())
+ c.commandParamsSchemas["Animation.setPaused"] = abxjsonschema.SchemaFor[AnimationSetPausedParams]()
+ c.commandResultSchemas["Animation.setPaused"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationSetPausedResult]())
+ c.commandParamsSchemas["Animation.setPlaybackRate"] = abxjsonschema.SchemaFor[AnimationSetPlaybackRateParams]()
+ c.commandResultSchemas["Animation.setPlaybackRate"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationSetPlaybackRateResult]())
+ c.commandParamsSchemas["Animation.setTiming"] = abxjsonschema.SchemaFor[AnimationSetTimingParams]()
+ c.commandResultSchemas["Animation.setTiming"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationSetTimingResult]())
+ c.commandParamsSchemas["Audits.getEncodedResponse"] = abxjsonschema.SchemaFor[AuditsGetEncodedResponseParams]()
+ c.commandResultSchemas["Audits.getEncodedResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsGetEncodedResponseResult]())
+ c.commandParamsSchemas["Audits.disable"] = abxjsonschema.SchemaFor[AuditsDisableParams]()
+ c.commandResultSchemas["Audits.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsDisableResult]())
+ c.commandParamsSchemas["Audits.enable"] = abxjsonschema.SchemaFor[AuditsEnableParams]()
+ c.commandResultSchemas["Audits.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsEnableResult]())
+ c.commandParamsSchemas["Audits.checkFormsIssues"] = abxjsonschema.SchemaFor[AuditsCheckFormsIssuesParams]()
+ c.commandResultSchemas["Audits.checkFormsIssues"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsCheckFormsIssuesResult]())
+ c.commandParamsSchemas["Autofill.trigger"] = abxjsonschema.SchemaFor[AutofillTriggerParams]()
+ c.commandResultSchemas["Autofill.trigger"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillTriggerResult]())
+ c.commandParamsSchemas["Autofill.setAddresses"] = abxjsonschema.SchemaFor[AutofillSetAddressesParams]()
+ c.commandResultSchemas["Autofill.setAddresses"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillSetAddressesResult]())
+ c.commandParamsSchemas["Autofill.disable"] = abxjsonschema.SchemaFor[AutofillDisableParams]()
+ c.commandResultSchemas["Autofill.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillDisableResult]())
+ c.commandParamsSchemas["Autofill.enable"] = abxjsonschema.SchemaFor[AutofillEnableParams]()
+ c.commandResultSchemas["Autofill.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillEnableResult]())
+ c.commandParamsSchemas["BackgroundService.startObserving"] = abxjsonschema.SchemaFor[BackgroundServiceStartObservingParams]()
+ c.commandResultSchemas["BackgroundService.startObserving"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceStartObservingResult]())
+ c.commandParamsSchemas["BackgroundService.stopObserving"] = abxjsonschema.SchemaFor[BackgroundServiceStopObservingParams]()
+ c.commandResultSchemas["BackgroundService.stopObserving"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceStopObservingResult]())
+ c.commandParamsSchemas["BackgroundService.setRecording"] = abxjsonschema.SchemaFor[BackgroundServiceSetRecordingParams]()
+ c.commandResultSchemas["BackgroundService.setRecording"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceSetRecordingResult]())
+ c.commandParamsSchemas["BackgroundService.clearEvents"] = abxjsonschema.SchemaFor[BackgroundServiceClearEventsParams]()
+ c.commandResultSchemas["BackgroundService.clearEvents"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceClearEventsResult]())
+ c.commandParamsSchemas["BluetoothEmulation.enable"] = abxjsonschema.SchemaFor[BluetoothEmulationEnableParams]()
+ c.commandResultSchemas["BluetoothEmulation.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationEnableResult]())
+ c.commandParamsSchemas["BluetoothEmulation.setSimulatedCentralState"] = abxjsonschema.SchemaFor[BluetoothEmulationSetSimulatedCentralStateParams]()
+ c.commandResultSchemas["BluetoothEmulation.setSimulatedCentralState"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSetSimulatedCentralStateResult]())
+ c.commandParamsSchemas["BluetoothEmulation.disable"] = abxjsonschema.SchemaFor[BluetoothEmulationDisableParams]()
+ c.commandResultSchemas["BluetoothEmulation.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationDisableResult]())
+ c.commandParamsSchemas["BluetoothEmulation.simulatePreconnectedPeripheral"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulatePreconnectedPeripheralParams]()
+ c.commandResultSchemas["BluetoothEmulation.simulatePreconnectedPeripheral"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulatePreconnectedPeripheralResult]())
+ c.commandParamsSchemas["BluetoothEmulation.simulateAdvertisement"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateAdvertisementParams]()
+ c.commandResultSchemas["BluetoothEmulation.simulateAdvertisement"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateAdvertisementResult]())
+ c.commandParamsSchemas["BluetoothEmulation.simulateGATTOperationResponse"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateGATTOperationResponseParams]()
+ c.commandResultSchemas["BluetoothEmulation.simulateGATTOperationResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateGATTOperationResponseResult]())
+ c.commandParamsSchemas["BluetoothEmulation.simulateCharacteristicOperationResponse"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateCharacteristicOperationResponseParams]()
+ c.commandResultSchemas["BluetoothEmulation.simulateCharacteristicOperationResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateCharacteristicOperationResponseResult]())
+ c.commandParamsSchemas["BluetoothEmulation.simulateDescriptorOperationResponse"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateDescriptorOperationResponseParams]()
+ c.commandResultSchemas["BluetoothEmulation.simulateDescriptorOperationResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateDescriptorOperationResponseResult]())
+ c.commandParamsSchemas["BluetoothEmulation.addService"] = abxjsonschema.SchemaFor[BluetoothEmulationAddServiceParams]()
+ c.commandResultSchemas["BluetoothEmulation.addService"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationAddServiceResult]())
+ c.commandParamsSchemas["BluetoothEmulation.removeService"] = abxjsonschema.SchemaFor[BluetoothEmulationRemoveServiceParams]()
+ c.commandResultSchemas["BluetoothEmulation.removeService"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationRemoveServiceResult]())
+ c.commandParamsSchemas["BluetoothEmulation.addCharacteristic"] = abxjsonschema.SchemaFor[BluetoothEmulationAddCharacteristicParams]()
+ c.commandResultSchemas["BluetoothEmulation.addCharacteristic"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationAddCharacteristicResult]())
+ c.commandParamsSchemas["BluetoothEmulation.removeCharacteristic"] = abxjsonschema.SchemaFor[BluetoothEmulationRemoveCharacteristicParams]()
+ c.commandResultSchemas["BluetoothEmulation.removeCharacteristic"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationRemoveCharacteristicResult]())
+ c.commandParamsSchemas["BluetoothEmulation.addDescriptor"] = abxjsonschema.SchemaFor[BluetoothEmulationAddDescriptorParams]()
+ c.commandResultSchemas["BluetoothEmulation.addDescriptor"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationAddDescriptorResult]())
+ c.commandParamsSchemas["BluetoothEmulation.removeDescriptor"] = abxjsonschema.SchemaFor[BluetoothEmulationRemoveDescriptorParams]()
+ c.commandResultSchemas["BluetoothEmulation.removeDescriptor"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationRemoveDescriptorResult]())
+ c.commandParamsSchemas["BluetoothEmulation.simulateGATTDisconnection"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateGATTDisconnectionParams]()
+ c.commandResultSchemas["BluetoothEmulation.simulateGATTDisconnection"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateGATTDisconnectionResult]())
+ c.commandParamsSchemas["Browser.setPermission"] = abxjsonschema.SchemaFor[BrowserSetPermissionParams]()
+ c.commandResultSchemas["Browser.setPermission"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetPermissionResult]())
+ c.commandParamsSchemas["Browser.grantPermissions"] = abxjsonschema.SchemaFor[BrowserGrantPermissionsParams]()
+ c.commandResultSchemas["Browser.grantPermissions"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGrantPermissionsResult]())
+ c.commandParamsSchemas["Browser.resetPermissions"] = abxjsonschema.SchemaFor[BrowserResetPermissionsParams]()
+ c.commandResultSchemas["Browser.resetPermissions"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserResetPermissionsResult]())
+ c.commandParamsSchemas["Browser.setDownloadBehavior"] = abxjsonschema.SchemaFor[BrowserSetDownloadBehaviorParams]()
+ c.commandResultSchemas["Browser.setDownloadBehavior"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetDownloadBehaviorResult]())
+ c.commandParamsSchemas["Browser.cancelDownload"] = abxjsonschema.SchemaFor[BrowserCancelDownloadParams]()
+ c.commandResultSchemas["Browser.cancelDownload"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserCancelDownloadResult]())
+ c.commandParamsSchemas["Browser.close"] = abxjsonschema.SchemaFor[BrowserCloseParams]()
+ c.commandResultSchemas["Browser.close"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserCloseResult]())
+ c.commandParamsSchemas["Browser.crash"] = abxjsonschema.SchemaFor[BrowserCrashParams]()
+ c.commandResultSchemas["Browser.crash"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserCrashResult]())
+ c.commandParamsSchemas["Browser.crashGpuProcess"] = abxjsonschema.SchemaFor[BrowserCrashGPUProcessParams]()
+ c.commandResultSchemas["Browser.crashGpuProcess"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserCrashGPUProcessResult]())
+ c.commandParamsSchemas["Browser.getVersion"] = abxjsonschema.SchemaFor[BrowserGetVersionParams]()
+ c.commandResultSchemas["Browser.getVersion"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetVersionResult]())
+ c.commandParamsSchemas["Browser.getBrowserCommandLine"] = abxjsonschema.SchemaFor[BrowserGetBrowserCommandLineParams]()
+ c.commandResultSchemas["Browser.getBrowserCommandLine"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetBrowserCommandLineResult]())
+ c.commandParamsSchemas["Browser.getHistograms"] = abxjsonschema.SchemaFor[BrowserGetHistogramsParams]()
+ c.commandResultSchemas["Browser.getHistograms"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetHistogramsResult]())
+ c.commandParamsSchemas["Browser.getHistogram"] = abxjsonschema.SchemaFor[BrowserGetHistogramParams]()
+ c.commandResultSchemas["Browser.getHistogram"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetHistogramResult]())
+ c.commandParamsSchemas["Browser.getWindowBounds"] = abxjsonschema.SchemaFor[BrowserGetWindowBoundsParams]()
+ c.commandResultSchemas["Browser.getWindowBounds"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetWindowBoundsResult]())
+ c.commandParamsSchemas["Browser.getWindowForTarget"] = abxjsonschema.SchemaFor[BrowserGetWindowForTargetParams]()
+ c.commandResultSchemas["Browser.getWindowForTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetWindowForTargetResult]())
+ c.commandParamsSchemas["Browser.setWindowBounds"] = abxjsonschema.SchemaFor[BrowserSetWindowBoundsParams]()
+ c.commandResultSchemas["Browser.setWindowBounds"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetWindowBoundsResult]())
+ c.commandParamsSchemas["Browser.setContentsSize"] = abxjsonschema.SchemaFor[BrowserSetContentsSizeParams]()
+ c.commandResultSchemas["Browser.setContentsSize"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetContentsSizeResult]())
+ c.commandParamsSchemas["Browser.setDockTile"] = abxjsonschema.SchemaFor[BrowserSetDockTileParams]()
+ c.commandResultSchemas["Browser.setDockTile"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetDockTileResult]())
+ c.commandParamsSchemas["Browser.executeBrowserCommand"] = abxjsonschema.SchemaFor[BrowserExecuteBrowserCommandParams]()
+ c.commandResultSchemas["Browser.executeBrowserCommand"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserExecuteBrowserCommandResult]())
+ c.commandParamsSchemas["Browser.addPrivacySandboxEnrollmentOverride"] = abxjsonschema.SchemaFor[BrowserAddPrivacySandboxEnrollmentOverrideParams]()
+ c.commandResultSchemas["Browser.addPrivacySandboxEnrollmentOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserAddPrivacySandboxEnrollmentOverrideResult]())
+ c.commandParamsSchemas["Browser.addPrivacySandboxCoordinatorKeyConfig"] = abxjsonschema.SchemaFor[BrowserAddPrivacySandboxCoordinatorKeyConfigParams]()
+ c.commandResultSchemas["Browser.addPrivacySandboxCoordinatorKeyConfig"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserAddPrivacySandboxCoordinatorKeyConfigResult]())
+ c.commandParamsSchemas["CSS.addRule"] = abxjsonschema.SchemaFor[CSSAddRuleParams]()
+ c.commandResultSchemas["CSS.addRule"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSAddRuleResult]())
+ c.commandParamsSchemas["CSS.collectClassNames"] = abxjsonschema.SchemaFor[CSSCollectClassNamesParams]()
+ c.commandResultSchemas["CSS.collectClassNames"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSCollectClassNamesResult]())
+ c.commandParamsSchemas["CSS.createStyleSheet"] = abxjsonschema.SchemaFor[CSSCreateStyleSheetParams]()
+ c.commandResultSchemas["CSS.createStyleSheet"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSCreateStyleSheetResult]())
+ c.commandParamsSchemas["CSS.disable"] = abxjsonschema.SchemaFor[CSSDisableParams]()
+ c.commandResultSchemas["CSS.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSDisableResult]())
+ c.commandParamsSchemas["CSS.enable"] = abxjsonschema.SchemaFor[CSSEnableParams]()
+ c.commandResultSchemas["CSS.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSEnableResult]())
+ c.commandParamsSchemas["CSS.forcePseudoState"] = abxjsonschema.SchemaFor[CSSForcePseudoStateParams]()
+ c.commandResultSchemas["CSS.forcePseudoState"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSForcePseudoStateResult]())
+ c.commandParamsSchemas["CSS.forceStartingStyle"] = abxjsonschema.SchemaFor[CSSForceStartingStyleParams]()
+ c.commandResultSchemas["CSS.forceStartingStyle"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSForceStartingStyleResult]())
+ c.commandParamsSchemas["CSS.getBackgroundColors"] = abxjsonschema.SchemaFor[CSSGetBackgroundColorsParams]()
+ c.commandResultSchemas["CSS.getBackgroundColors"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetBackgroundColorsResult]())
+ c.commandParamsSchemas["CSS.getComputedStyleForNode"] = abxjsonschema.SchemaFor[CSSGetComputedStyleForNodeParams]()
+ c.commandResultSchemas["CSS.getComputedStyleForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetComputedStyleForNodeResult]())
+ c.commandParamsSchemas["CSS.resolveValues"] = abxjsonschema.SchemaFor[CSSResolveValuesParams]()
+ c.commandResultSchemas["CSS.resolveValues"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSResolveValuesResult]())
+ c.commandParamsSchemas["CSS.getLonghandProperties"] = abxjsonschema.SchemaFor[CSSGetLonghandPropertiesParams]()
+ c.commandResultSchemas["CSS.getLonghandProperties"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetLonghandPropertiesResult]())
+ c.commandParamsSchemas["CSS.getInlineStylesForNode"] = abxjsonschema.SchemaFor[CSSGetInlineStylesForNodeParams]()
+ c.commandResultSchemas["CSS.getInlineStylesForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetInlineStylesForNodeResult]())
+ c.commandParamsSchemas["CSS.getAnimatedStylesForNode"] = abxjsonschema.SchemaFor[CSSGetAnimatedStylesForNodeParams]()
+ c.commandResultSchemas["CSS.getAnimatedStylesForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetAnimatedStylesForNodeResult]())
+ c.commandParamsSchemas["CSS.getMatchedStylesForNode"] = abxjsonschema.SchemaFor[CSSGetMatchedStylesForNodeParams]()
+ c.commandResultSchemas["CSS.getMatchedStylesForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetMatchedStylesForNodeResult]())
+ c.commandParamsSchemas["CSS.getEnvironmentVariables"] = abxjsonschema.SchemaFor[CSSGetEnvironmentVariablesParams]()
+ c.commandResultSchemas["CSS.getEnvironmentVariables"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetEnvironmentVariablesResult]())
+ c.commandParamsSchemas["CSS.getMediaQueries"] = abxjsonschema.SchemaFor[CSSGetMediaQueriesParams]()
+ c.commandResultSchemas["CSS.getMediaQueries"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetMediaQueriesResult]())
+ c.commandParamsSchemas["CSS.getPlatformFontsForNode"] = abxjsonschema.SchemaFor[CSSGetPlatformFontsForNodeParams]()
+ c.commandResultSchemas["CSS.getPlatformFontsForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetPlatformFontsForNodeResult]())
+ c.commandParamsSchemas["CSS.getStyleSheetText"] = abxjsonschema.SchemaFor[CSSGetStyleSheetTextParams]()
+ c.commandResultSchemas["CSS.getStyleSheetText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetStyleSheetTextResult]())
+ c.commandParamsSchemas["CSS.getLayersForNode"] = abxjsonschema.SchemaFor[CSSGetLayersForNodeParams]()
+ c.commandResultSchemas["CSS.getLayersForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetLayersForNodeResult]())
+ c.commandParamsSchemas["CSS.getLocationForSelector"] = abxjsonschema.SchemaFor[CSSGetLocationForSelectorParams]()
+ c.commandResultSchemas["CSS.getLocationForSelector"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetLocationForSelectorResult]())
+ c.commandParamsSchemas["CSS.trackComputedStyleUpdatesForNode"] = abxjsonschema.SchemaFor[CSSTrackComputedStyleUpdatesForNodeParams]()
+ c.commandResultSchemas["CSS.trackComputedStyleUpdatesForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSTrackComputedStyleUpdatesForNodeResult]())
+ c.commandParamsSchemas["CSS.trackComputedStyleUpdates"] = abxjsonschema.SchemaFor[CSSTrackComputedStyleUpdatesParams]()
+ c.commandResultSchemas["CSS.trackComputedStyleUpdates"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSTrackComputedStyleUpdatesResult]())
+ c.commandParamsSchemas["CSS.takeComputedStyleUpdates"] = abxjsonschema.SchemaFor[CSSTakeComputedStyleUpdatesParams]()
+ c.commandResultSchemas["CSS.takeComputedStyleUpdates"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSTakeComputedStyleUpdatesResult]())
+ c.commandParamsSchemas["CSS.setEffectivePropertyValueForNode"] = abxjsonschema.SchemaFor[CSSSetEffectivePropertyValueForNodeParams]()
+ c.commandResultSchemas["CSS.setEffectivePropertyValueForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetEffectivePropertyValueForNodeResult]())
+ c.commandParamsSchemas["CSS.setPropertyRulePropertyName"] = abxjsonschema.SchemaFor[CSSSetPropertyRulePropertyNameParams]()
+ c.commandResultSchemas["CSS.setPropertyRulePropertyName"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetPropertyRulePropertyNameResult]())
+ c.commandParamsSchemas["CSS.setKeyframeKey"] = abxjsonschema.SchemaFor[CSSSetKeyframeKeyParams]()
+ c.commandResultSchemas["CSS.setKeyframeKey"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetKeyframeKeyResult]())
+ c.commandParamsSchemas["CSS.setMediaText"] = abxjsonschema.SchemaFor[CSSSetMediaTextParams]()
+ c.commandResultSchemas["CSS.setMediaText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetMediaTextResult]())
+ c.commandParamsSchemas["CSS.setContainerQueryText"] = abxjsonschema.SchemaFor[CSSSetContainerQueryTextParams]()
+ c.commandResultSchemas["CSS.setContainerQueryText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetContainerQueryTextResult]())
+ c.commandParamsSchemas["CSS.setSupportsText"] = abxjsonschema.SchemaFor[CSSSetSupportsTextParams]()
+ c.commandResultSchemas["CSS.setSupportsText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetSupportsTextResult]())
+ c.commandParamsSchemas["CSS.setNavigationText"] = abxjsonschema.SchemaFor[CSSSetNavigationTextParams]()
+ c.commandResultSchemas["CSS.setNavigationText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetNavigationTextResult]())
+ c.commandParamsSchemas["CSS.setScopeText"] = abxjsonschema.SchemaFor[CSSSetScopeTextParams]()
+ c.commandResultSchemas["CSS.setScopeText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetScopeTextResult]())
+ c.commandParamsSchemas["CSS.setRuleSelector"] = abxjsonschema.SchemaFor[CSSSetRuleSelectorParams]()
+ c.commandResultSchemas["CSS.setRuleSelector"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetRuleSelectorResult]())
+ c.commandParamsSchemas["CSS.setStyleSheetText"] = abxjsonschema.SchemaFor[CSSSetStyleSheetTextParams]()
+ c.commandResultSchemas["CSS.setStyleSheetText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetStyleSheetTextResult]())
+ c.commandParamsSchemas["CSS.setStyleTexts"] = abxjsonschema.SchemaFor[CSSSetStyleTextsParams]()
+ c.commandResultSchemas["CSS.setStyleTexts"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetStyleTextsResult]())
+ c.commandParamsSchemas["CSS.startRuleUsageTracking"] = abxjsonschema.SchemaFor[CSSStartRuleUsageTrackingParams]()
+ c.commandResultSchemas["CSS.startRuleUsageTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStartRuleUsageTrackingResult]())
+ c.commandParamsSchemas["CSS.stopRuleUsageTracking"] = abxjsonschema.SchemaFor[CSSStopRuleUsageTrackingParams]()
+ c.commandResultSchemas["CSS.stopRuleUsageTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStopRuleUsageTrackingResult]())
+ c.commandParamsSchemas["CSS.takeCoverageDelta"] = abxjsonschema.SchemaFor[CSSTakeCoverageDeltaParams]()
+ c.commandResultSchemas["CSS.takeCoverageDelta"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSTakeCoverageDeltaResult]())
+ c.commandParamsSchemas["CSS.setLocalFontsEnabled"] = abxjsonschema.SchemaFor[CSSSetLocalFontsEnabledParams]()
+ c.commandResultSchemas["CSS.setLocalFontsEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetLocalFontsEnabledResult]())
+ c.commandParamsSchemas["CacheStorage.deleteCache"] = abxjsonschema.SchemaFor[CacheStorageDeleteCacheParams]()
+ c.commandResultSchemas["CacheStorage.deleteCache"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageDeleteCacheResult]())
+ c.commandParamsSchemas["CacheStorage.deleteEntry"] = abxjsonschema.SchemaFor[CacheStorageDeleteEntryParams]()
+ c.commandResultSchemas["CacheStorage.deleteEntry"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageDeleteEntryResult]())
+ c.commandParamsSchemas["CacheStorage.requestCacheNames"] = abxjsonschema.SchemaFor[CacheStorageRequestCacheNamesParams]()
+ c.commandResultSchemas["CacheStorage.requestCacheNames"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageRequestCacheNamesResult]())
+ c.commandParamsSchemas["CacheStorage.requestCachedResponse"] = abxjsonschema.SchemaFor[CacheStorageRequestCachedResponseParams]()
+ c.commandResultSchemas["CacheStorage.requestCachedResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageRequestCachedResponseResult]())
+ c.commandParamsSchemas["CacheStorage.requestEntries"] = abxjsonschema.SchemaFor[CacheStorageRequestEntriesParams]()
+ c.commandResultSchemas["CacheStorage.requestEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageRequestEntriesResult]())
+ c.commandParamsSchemas["Cast.enable"] = abxjsonschema.SchemaFor[CastEnableParams]()
+ c.commandResultSchemas["Cast.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[CastEnableResult]())
+ c.commandParamsSchemas["Cast.disable"] = abxjsonschema.SchemaFor[CastDisableParams]()
+ c.commandResultSchemas["Cast.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[CastDisableResult]())
+ c.commandParamsSchemas["Cast.setSinkToUse"] = abxjsonschema.SchemaFor[CastSetSinkToUseParams]()
+ c.commandResultSchemas["Cast.setSinkToUse"] = nativeResultSchema(abxjsonschema.SchemaFor[CastSetSinkToUseResult]())
+ c.commandParamsSchemas["Cast.startDesktopMirroring"] = abxjsonschema.SchemaFor[CastStartDesktopMirroringParams]()
+ c.commandResultSchemas["Cast.startDesktopMirroring"] = nativeResultSchema(abxjsonschema.SchemaFor[CastStartDesktopMirroringResult]())
+ c.commandParamsSchemas["Cast.startTabMirroring"] = abxjsonschema.SchemaFor[CastStartTabMirroringParams]()
+ c.commandResultSchemas["Cast.startTabMirroring"] = nativeResultSchema(abxjsonschema.SchemaFor[CastStartTabMirroringResult]())
+ c.commandParamsSchemas["Cast.stopCasting"] = abxjsonschema.SchemaFor[CastStopCastingParams]()
+ c.commandResultSchemas["Cast.stopCasting"] = nativeResultSchema(abxjsonschema.SchemaFor[CastStopCastingResult]())
+ c.commandParamsSchemas["Console.clearMessages"] = abxjsonschema.SchemaFor[ConsoleClearMessagesParams]()
+ c.commandResultSchemas["Console.clearMessages"] = nativeResultSchema(abxjsonschema.SchemaFor[ConsoleClearMessagesResult]())
+ c.commandParamsSchemas["Console.disable"] = abxjsonschema.SchemaFor[ConsoleDisableParams]()
+ c.commandResultSchemas["Console.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[ConsoleDisableResult]())
+ c.commandParamsSchemas["Console.enable"] = abxjsonschema.SchemaFor[ConsoleEnableParams]()
+ c.commandResultSchemas["Console.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[ConsoleEnableResult]())
+ c.commandParamsSchemas["DOM.collectClassNamesFromSubtree"] = abxjsonschema.SchemaFor[DOMCollectClassNamesFromSubtreeParams]()
+ c.commandResultSchemas["DOM.collectClassNamesFromSubtree"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMCollectClassNamesFromSubtreeResult]())
+ c.commandParamsSchemas["DOM.copyTo"] = abxjsonschema.SchemaFor[DOMCopyToParams]()
+ c.commandResultSchemas["DOM.copyTo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMCopyToResult]())
+ c.commandParamsSchemas["DOM.describeNode"] = abxjsonschema.SchemaFor[DOMDescribeNodeParams]()
+ c.commandResultSchemas["DOM.describeNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDescribeNodeResult]())
+ c.commandParamsSchemas["DOM.scrollIntoViewIfNeeded"] = abxjsonschema.SchemaFor[DOMScrollIntoViewIfNeededParams]()
+ c.commandResultSchemas["DOM.scrollIntoViewIfNeeded"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMScrollIntoViewIfNeededResult]())
+ c.commandParamsSchemas["DOM.disable"] = abxjsonschema.SchemaFor[DOMDisableParams]()
+ c.commandResultSchemas["DOM.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDisableResult]())
+ c.commandParamsSchemas["DOM.discardSearchResults"] = abxjsonschema.SchemaFor[DOMDiscardSearchResultsParams]()
+ c.commandResultSchemas["DOM.discardSearchResults"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDiscardSearchResultsResult]())
+ c.commandParamsSchemas["DOM.enable"] = abxjsonschema.SchemaFor[DOMEnableParams]()
+ c.commandResultSchemas["DOM.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMEnableResult]())
+ c.commandParamsSchemas["DOM.focus"] = abxjsonschema.SchemaFor[DOMFocusParams]()
+ c.commandResultSchemas["DOM.focus"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMFocusResult]())
+ c.commandParamsSchemas["DOM.getAttributes"] = abxjsonschema.SchemaFor[DOMGetAttributesParams]()
+ c.commandResultSchemas["DOM.getAttributes"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetAttributesResult]())
+ c.commandParamsSchemas["DOM.getBoxModel"] = abxjsonschema.SchemaFor[DOMGetBoxModelParams]()
+ c.commandResultSchemas["DOM.getBoxModel"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetBoxModelResult]())
+ c.commandParamsSchemas["DOM.getContentQuads"] = abxjsonschema.SchemaFor[DOMGetContentQuadsParams]()
+ c.commandResultSchemas["DOM.getContentQuads"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetContentQuadsResult]())
+ c.commandParamsSchemas["DOM.getDocument"] = abxjsonschema.SchemaFor[DOMGetDocumentParams]()
+ c.commandResultSchemas["DOM.getDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetDocumentResult]())
+ c.commandParamsSchemas["DOM.getFlattenedDocument"] = abxjsonschema.SchemaFor[DOMGetFlattenedDocumentParams]()
+ c.commandResultSchemas["DOM.getFlattenedDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetFlattenedDocumentResult]())
+ c.commandParamsSchemas["DOM.getNodesForSubtreeByStyle"] = abxjsonschema.SchemaFor[DOMGetNodesForSubtreeByStyleParams]()
+ c.commandResultSchemas["DOM.getNodesForSubtreeByStyle"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetNodesForSubtreeByStyleResult]())
+ c.commandParamsSchemas["DOM.getNodeForLocation"] = abxjsonschema.SchemaFor[DOMGetNodeForLocationParams]()
+ c.commandResultSchemas["DOM.getNodeForLocation"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetNodeForLocationResult]())
+ c.commandParamsSchemas["DOM.getOuterHTML"] = abxjsonschema.SchemaFor[DOMGetOuterHTMLParams]()
+ c.commandResultSchemas["DOM.getOuterHTML"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetOuterHTMLResult]())
+ c.commandParamsSchemas["DOM.getRelayoutBoundary"] = abxjsonschema.SchemaFor[DOMGetRelayoutBoundaryParams]()
+ c.commandResultSchemas["DOM.getRelayoutBoundary"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetRelayoutBoundaryResult]())
+ c.commandParamsSchemas["DOM.getSearchResults"] = abxjsonschema.SchemaFor[DOMGetSearchResultsParams]()
+ c.commandResultSchemas["DOM.getSearchResults"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetSearchResultsResult]())
+ c.commandParamsSchemas["DOM.hideHighlight"] = abxjsonschema.SchemaFor[DOMHideHighlightParams]()
+ c.commandResultSchemas["DOM.hideHighlight"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMHideHighlightResult]())
+ c.commandParamsSchemas["DOM.highlightNode"] = abxjsonschema.SchemaFor[DOMHighlightNodeParams]()
+ c.commandResultSchemas["DOM.highlightNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMHighlightNodeResult]())
+ c.commandParamsSchemas["DOM.highlightRect"] = abxjsonschema.SchemaFor[DOMHighlightRectParams]()
+ c.commandResultSchemas["DOM.highlightRect"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMHighlightRectResult]())
+ c.commandParamsSchemas["DOM.markUndoableState"] = abxjsonschema.SchemaFor[DOMMarkUndoableStateParams]()
+ c.commandResultSchemas["DOM.markUndoableState"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMMarkUndoableStateResult]())
+ c.commandParamsSchemas["DOM.moveTo"] = abxjsonschema.SchemaFor[DOMMoveToParams]()
+ c.commandResultSchemas["DOM.moveTo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMMoveToResult]())
+ c.commandParamsSchemas["DOM.performSearch"] = abxjsonschema.SchemaFor[DOMPerformSearchParams]()
+ c.commandResultSchemas["DOM.performSearch"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPerformSearchResult]())
+ c.commandParamsSchemas["DOM.pushNodeByPathToFrontend"] = abxjsonschema.SchemaFor[DOMPushNodeByPathToFrontendParams]()
+ c.commandResultSchemas["DOM.pushNodeByPathToFrontend"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPushNodeByPathToFrontendResult]())
+ c.commandParamsSchemas["DOM.pushNodesByBackendIdsToFrontend"] = abxjsonschema.SchemaFor[DOMPushNodesByBackendIdsToFrontendParams]()
+ c.commandResultSchemas["DOM.pushNodesByBackendIdsToFrontend"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPushNodesByBackendIdsToFrontendResult]())
+ c.commandParamsSchemas["DOM.querySelector"] = abxjsonschema.SchemaFor[DOMQuerySelectorParams]()
+ c.commandResultSchemas["DOM.querySelector"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMQuerySelectorResult]())
+ c.commandParamsSchemas["DOM.querySelectorAll"] = abxjsonschema.SchemaFor[DOMQuerySelectorAllParams]()
+ c.commandResultSchemas["DOM.querySelectorAll"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMQuerySelectorAllResult]())
+ c.commandParamsSchemas["DOM.getTopLayerElements"] = abxjsonschema.SchemaFor[DOMGetTopLayerElementsParams]()
+ c.commandResultSchemas["DOM.getTopLayerElements"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetTopLayerElementsResult]())
+ c.commandParamsSchemas["DOM.getElementByRelation"] = abxjsonschema.SchemaFor[DOMGetElementByRelationParams]()
+ c.commandResultSchemas["DOM.getElementByRelation"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetElementByRelationResult]())
+ c.commandParamsSchemas["DOM.redo"] = abxjsonschema.SchemaFor[DOMRedoParams]()
+ c.commandResultSchemas["DOM.redo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRedoResult]())
+ c.commandParamsSchemas["DOM.removeAttribute"] = abxjsonschema.SchemaFor[DOMRemoveAttributeParams]()
+ c.commandResultSchemas["DOM.removeAttribute"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRemoveAttributeResult]())
+ c.commandParamsSchemas["DOM.removeNode"] = abxjsonschema.SchemaFor[DOMRemoveNodeParams]()
+ c.commandResultSchemas["DOM.removeNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRemoveNodeResult]())
+ c.commandParamsSchemas["DOM.requestChildNodes"] = abxjsonschema.SchemaFor[DOMRequestChildNodesParams]()
+ c.commandResultSchemas["DOM.requestChildNodes"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRequestChildNodesResult]())
+ c.commandParamsSchemas["DOM.requestNode"] = abxjsonschema.SchemaFor[DOMRequestNodeParams]()
+ c.commandResultSchemas["DOM.requestNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRequestNodeResult]())
+ c.commandParamsSchemas["DOM.resolveNode"] = abxjsonschema.SchemaFor[DOMResolveNodeParams]()
+ c.commandResultSchemas["DOM.resolveNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMResolveNodeResult]())
+ c.commandParamsSchemas["DOM.setAttributeValue"] = abxjsonschema.SchemaFor[DOMSetAttributeValueParams]()
+ c.commandResultSchemas["DOM.setAttributeValue"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetAttributeValueResult]())
+ c.commandParamsSchemas["DOM.setAttributesAsText"] = abxjsonschema.SchemaFor[DOMSetAttributesAsTextParams]()
+ c.commandResultSchemas["DOM.setAttributesAsText"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetAttributesAsTextResult]())
+ c.commandParamsSchemas["DOM.setFileInputFiles"] = abxjsonschema.SchemaFor[DOMSetFileInputFilesParams]()
+ c.commandResultSchemas["DOM.setFileInputFiles"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetFileInputFilesResult]())
+ c.commandParamsSchemas["DOM.setNodeStackTracesEnabled"] = abxjsonschema.SchemaFor[DOMSetNodeStackTracesEnabledParams]()
+ c.commandResultSchemas["DOM.setNodeStackTracesEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetNodeStackTracesEnabledResult]())
+ c.commandParamsSchemas["DOM.getNodeStackTraces"] = abxjsonschema.SchemaFor[DOMGetNodeStackTracesParams]()
+ c.commandResultSchemas["DOM.getNodeStackTraces"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetNodeStackTracesResult]())
+ c.commandParamsSchemas["DOM.getFileInfo"] = abxjsonschema.SchemaFor[DOMGetFileInfoParams]()
+ c.commandResultSchemas["DOM.getFileInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetFileInfoResult]())
+ c.commandParamsSchemas["DOM.getDetachedDomNodes"] = abxjsonschema.SchemaFor[DOMGetDetachedDOMNodesParams]()
+ c.commandResultSchemas["DOM.getDetachedDomNodes"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetDetachedDOMNodesResult]())
+ c.commandParamsSchemas["DOM.setInspectedNode"] = abxjsonschema.SchemaFor[DOMSetInspectedNodeParams]()
+ c.commandResultSchemas["DOM.setInspectedNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetInspectedNodeResult]())
+ c.commandParamsSchemas["DOM.setNodeName"] = abxjsonschema.SchemaFor[DOMSetNodeNameParams]()
+ c.commandResultSchemas["DOM.setNodeName"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetNodeNameResult]())
+ c.commandParamsSchemas["DOM.setNodeValue"] = abxjsonschema.SchemaFor[DOMSetNodeValueParams]()
+ c.commandResultSchemas["DOM.setNodeValue"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetNodeValueResult]())
+ c.commandParamsSchemas["DOM.setOuterHTML"] = abxjsonschema.SchemaFor[DOMSetOuterHTMLParams]()
+ c.commandResultSchemas["DOM.setOuterHTML"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetOuterHTMLResult]())
+ c.commandParamsSchemas["DOM.undo"] = abxjsonschema.SchemaFor[DOMUndoParams]()
+ c.commandResultSchemas["DOM.undo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMUndoResult]())
+ c.commandParamsSchemas["DOM.getFrameOwner"] = abxjsonschema.SchemaFor[DOMGetFrameOwnerParams]()
+ c.commandResultSchemas["DOM.getFrameOwner"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetFrameOwnerResult]())
+ c.commandParamsSchemas["DOM.getContainerForNode"] = abxjsonschema.SchemaFor[DOMGetContainerForNodeParams]()
+ c.commandResultSchemas["DOM.getContainerForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetContainerForNodeResult]())
+ c.commandParamsSchemas["DOM.getQueryingDescendantsForContainer"] = abxjsonschema.SchemaFor[DOMGetQueryingDescendantsForContainerParams]()
+ c.commandResultSchemas["DOM.getQueryingDescendantsForContainer"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetQueryingDescendantsForContainerResult]())
+ c.commandParamsSchemas["DOM.getAnchorElement"] = abxjsonschema.SchemaFor[DOMGetAnchorElementParams]()
+ c.commandResultSchemas["DOM.getAnchorElement"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetAnchorElementResult]())
+ c.commandParamsSchemas["DOM.forceShowPopover"] = abxjsonschema.SchemaFor[DOMForceShowPopoverParams]()
+ c.commandResultSchemas["DOM.forceShowPopover"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMForceShowPopoverResult]())
+ c.commandParamsSchemas["DOMDebugger.getEventListeners"] = abxjsonschema.SchemaFor[DOMDebuggerGetEventListenersParams]()
+ c.commandResultSchemas["DOMDebugger.getEventListeners"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerGetEventListenersResult]())
+ c.commandParamsSchemas["DOMDebugger.removeDOMBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerRemoveDOMBreakpointParams]()
+ c.commandResultSchemas["DOMDebugger.removeDOMBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerRemoveDOMBreakpointResult]())
+ c.commandParamsSchemas["DOMDebugger.removeEventListenerBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerRemoveEventListenerBreakpointParams]()
+ c.commandResultSchemas["DOMDebugger.removeEventListenerBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerRemoveEventListenerBreakpointResult]())
+ c.commandParamsSchemas["DOMDebugger.removeInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerRemoveInstrumentationBreakpointParams]()
+ c.commandResultSchemas["DOMDebugger.removeInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerRemoveInstrumentationBreakpointResult]())
+ c.commandParamsSchemas["DOMDebugger.removeXHRBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerRemoveXHRBreakpointParams]()
+ c.commandResultSchemas["DOMDebugger.removeXHRBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerRemoveXHRBreakpointResult]())
+ c.commandParamsSchemas["DOMDebugger.setBreakOnCSPViolation"] = abxjsonschema.SchemaFor[DOMDebuggerSetBreakOnCSPViolationParams]()
+ c.commandResultSchemas["DOMDebugger.setBreakOnCSPViolation"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetBreakOnCSPViolationResult]())
+ c.commandParamsSchemas["DOMDebugger.setDOMBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerSetDOMBreakpointParams]()
+ c.commandResultSchemas["DOMDebugger.setDOMBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetDOMBreakpointResult]())
+ c.commandParamsSchemas["DOMDebugger.setEventListenerBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerSetEventListenerBreakpointParams]()
+ c.commandResultSchemas["DOMDebugger.setEventListenerBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetEventListenerBreakpointResult]())
+ c.commandParamsSchemas["DOMDebugger.setInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerSetInstrumentationBreakpointParams]()
+ c.commandResultSchemas["DOMDebugger.setInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetInstrumentationBreakpointResult]())
+ c.commandParamsSchemas["DOMDebugger.setXHRBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerSetXHRBreakpointParams]()
+ c.commandResultSchemas["DOMDebugger.setXHRBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetXHRBreakpointResult]())
+ c.commandParamsSchemas["DOMSnapshot.disable"] = abxjsonschema.SchemaFor[DOMSnapshotDisableParams]()
+ c.commandResultSchemas["DOMSnapshot.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSnapshotDisableResult]())
+ c.commandParamsSchemas["DOMSnapshot.enable"] = abxjsonschema.SchemaFor[DOMSnapshotEnableParams]()
+ c.commandResultSchemas["DOMSnapshot.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSnapshotEnableResult]())
+ c.commandParamsSchemas["DOMSnapshot.getSnapshot"] = abxjsonschema.SchemaFor[DOMSnapshotGetSnapshotParams]()
+ c.commandResultSchemas["DOMSnapshot.getSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSnapshotGetSnapshotResult]())
+ c.commandParamsSchemas["DOMSnapshot.captureSnapshot"] = abxjsonschema.SchemaFor[DOMSnapshotCaptureSnapshotParams]()
+ c.commandResultSchemas["DOMSnapshot.captureSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSnapshotCaptureSnapshotResult]())
+ c.commandParamsSchemas["DOMStorage.clear"] = abxjsonschema.SchemaFor[DOMStorageClearParams]()
+ c.commandResultSchemas["DOMStorage.clear"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageClearResult]())
+ c.commandParamsSchemas["DOMStorage.disable"] = abxjsonschema.SchemaFor[DOMStorageDisableParams]()
+ c.commandResultSchemas["DOMStorage.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDisableResult]())
+ c.commandParamsSchemas["DOMStorage.enable"] = abxjsonschema.SchemaFor[DOMStorageEnableParams]()
+ c.commandResultSchemas["DOMStorage.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageEnableResult]())
+ c.commandParamsSchemas["DOMStorage.getDOMStorageItems"] = abxjsonschema.SchemaFor[DOMStorageGetDOMStorageItemsParams]()
+ c.commandResultSchemas["DOMStorage.getDOMStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageGetDOMStorageItemsResult]())
+ c.commandParamsSchemas["DOMStorage.removeDOMStorageItem"] = abxjsonschema.SchemaFor[DOMStorageRemoveDOMStorageItemParams]()
+ c.commandResultSchemas["DOMStorage.removeDOMStorageItem"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageRemoveDOMStorageItemResult]())
+ c.commandParamsSchemas["DOMStorage.setDOMStorageItem"] = abxjsonschema.SchemaFor[DOMStorageSetDOMStorageItemParams]()
+ c.commandResultSchemas["DOMStorage.setDOMStorageItem"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageSetDOMStorageItemResult]())
+ c.commandParamsSchemas["Debugger.continueToLocation"] = abxjsonschema.SchemaFor[DebuggerContinueToLocationParams]()
+ c.commandResultSchemas["Debugger.continueToLocation"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerContinueToLocationResult]())
+ c.commandParamsSchemas["Debugger.disable"] = abxjsonschema.SchemaFor[DebuggerDisableParams]()
+ c.commandResultSchemas["Debugger.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerDisableResult]())
+ c.commandParamsSchemas["Debugger.enable"] = abxjsonschema.SchemaFor[DebuggerEnableParams]()
+ c.commandResultSchemas["Debugger.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerEnableResult]())
+ c.commandParamsSchemas["Debugger.evaluateOnCallFrame"] = abxjsonschema.SchemaFor[DebuggerEvaluateOnCallFrameParams]()
+ c.commandResultSchemas["Debugger.evaluateOnCallFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerEvaluateOnCallFrameResult]())
+ c.commandParamsSchemas["Debugger.getPossibleBreakpoints"] = abxjsonschema.SchemaFor[DebuggerGetPossibleBreakpointsParams]()
+ c.commandResultSchemas["Debugger.getPossibleBreakpoints"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerGetPossibleBreakpointsResult]())
+ c.commandParamsSchemas["Debugger.getScriptSource"] = abxjsonschema.SchemaFor[DebuggerGetScriptSourceParams]()
+ c.commandResultSchemas["Debugger.getScriptSource"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerGetScriptSourceResult]())
+ c.commandParamsSchemas["Debugger.disassembleWasmModule"] = abxjsonschema.SchemaFor[DebuggerDisassembleWasmModuleParams]()
+ c.commandResultSchemas["Debugger.disassembleWasmModule"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerDisassembleWasmModuleResult]())
+ c.commandParamsSchemas["Debugger.nextWasmDisassemblyChunk"] = abxjsonschema.SchemaFor[DebuggerNextWasmDisassemblyChunkParams]()
+ c.commandResultSchemas["Debugger.nextWasmDisassemblyChunk"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerNextWasmDisassemblyChunkResult]())
+ c.commandParamsSchemas["Debugger.getWasmBytecode"] = abxjsonschema.SchemaFor[DebuggerGetWasmBytecodeParams]()
+ c.commandResultSchemas["Debugger.getWasmBytecode"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerGetWasmBytecodeResult]())
+ c.commandParamsSchemas["Debugger.getStackTrace"] = abxjsonschema.SchemaFor[DebuggerGetStackTraceParams]()
+ c.commandResultSchemas["Debugger.getStackTrace"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerGetStackTraceResult]())
+ c.commandParamsSchemas["Debugger.pause"] = abxjsonschema.SchemaFor[DebuggerPauseParams]()
+ c.commandResultSchemas["Debugger.pause"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerPauseResult]())
+ c.commandParamsSchemas["Debugger.pauseOnAsyncCall"] = abxjsonschema.SchemaFor[DebuggerPauseOnAsyncCallParams]()
+ c.commandResultSchemas["Debugger.pauseOnAsyncCall"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerPauseOnAsyncCallResult]())
+ c.commandParamsSchemas["Debugger.removeBreakpoint"] = abxjsonschema.SchemaFor[DebuggerRemoveBreakpointParams]()
+ c.commandResultSchemas["Debugger.removeBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerRemoveBreakpointResult]())
+ c.commandParamsSchemas["Debugger.restartFrame"] = abxjsonschema.SchemaFor[DebuggerRestartFrameParams]()
+ c.commandResultSchemas["Debugger.restartFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerRestartFrameResult]())
+ c.commandParamsSchemas["Debugger.resume"] = abxjsonschema.SchemaFor[DebuggerResumeParams]()
+ c.commandResultSchemas["Debugger.resume"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerResumeResult]())
+ c.commandParamsSchemas["Debugger.searchInContent"] = abxjsonschema.SchemaFor[DebuggerSearchInContentParams]()
+ c.commandResultSchemas["Debugger.searchInContent"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSearchInContentResult]())
+ c.commandParamsSchemas["Debugger.setAsyncCallStackDepth"] = abxjsonschema.SchemaFor[DebuggerSetAsyncCallStackDepthParams]()
+ c.commandResultSchemas["Debugger.setAsyncCallStackDepth"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetAsyncCallStackDepthResult]())
+ c.commandParamsSchemas["Debugger.setBlackboxExecutionContexts"] = abxjsonschema.SchemaFor[DebuggerSetBlackboxExecutionContextsParams]()
+ c.commandResultSchemas["Debugger.setBlackboxExecutionContexts"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBlackboxExecutionContextsResult]())
+ c.commandParamsSchemas["Debugger.setBlackboxPatterns"] = abxjsonschema.SchemaFor[DebuggerSetBlackboxPatternsParams]()
+ c.commandResultSchemas["Debugger.setBlackboxPatterns"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBlackboxPatternsResult]())
+ c.commandParamsSchemas["Debugger.setBlackboxedRanges"] = abxjsonschema.SchemaFor[DebuggerSetBlackboxedRangesParams]()
+ c.commandResultSchemas["Debugger.setBlackboxedRanges"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBlackboxedRangesResult]())
+ c.commandParamsSchemas["Debugger.setBreakpoint"] = abxjsonschema.SchemaFor[DebuggerSetBreakpointParams]()
+ c.commandResultSchemas["Debugger.setBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBreakpointResult]())
+ c.commandParamsSchemas["Debugger.setInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[DebuggerSetInstrumentationBreakpointParams]()
+ c.commandResultSchemas["Debugger.setInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetInstrumentationBreakpointResult]())
+ c.commandParamsSchemas["Debugger.setBreakpointByUrl"] = abxjsonschema.SchemaFor[DebuggerSetBreakpointByURLParams]()
+ c.commandResultSchemas["Debugger.setBreakpointByUrl"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBreakpointByURLResult]())
+ c.commandParamsSchemas["Debugger.setBreakpointOnFunctionCall"] = abxjsonschema.SchemaFor[DebuggerSetBreakpointOnFunctionCallParams]()
+ c.commandResultSchemas["Debugger.setBreakpointOnFunctionCall"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBreakpointOnFunctionCallResult]())
+ c.commandParamsSchemas["Debugger.setBreakpointsActive"] = abxjsonschema.SchemaFor[DebuggerSetBreakpointsActiveParams]()
+ c.commandResultSchemas["Debugger.setBreakpointsActive"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBreakpointsActiveResult]())
+ c.commandParamsSchemas["Debugger.setPauseOnExceptions"] = abxjsonschema.SchemaFor[DebuggerSetPauseOnExceptionsParams]()
+ c.commandResultSchemas["Debugger.setPauseOnExceptions"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetPauseOnExceptionsResult]())
+ c.commandParamsSchemas["Debugger.setReturnValue"] = abxjsonschema.SchemaFor[DebuggerSetReturnValueParams]()
+ c.commandResultSchemas["Debugger.setReturnValue"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetReturnValueResult]())
+ c.commandParamsSchemas["Debugger.setScriptSource"] = abxjsonschema.SchemaFor[DebuggerSetScriptSourceParams]()
+ c.commandResultSchemas["Debugger.setScriptSource"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetScriptSourceResult]())
+ c.commandParamsSchemas["Debugger.setSkipAllPauses"] = abxjsonschema.SchemaFor[DebuggerSetSkipAllPausesParams]()
+ c.commandResultSchemas["Debugger.setSkipAllPauses"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetSkipAllPausesResult]())
+ c.commandParamsSchemas["Debugger.setVariableValue"] = abxjsonschema.SchemaFor[DebuggerSetVariableValueParams]()
+ c.commandResultSchemas["Debugger.setVariableValue"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetVariableValueResult]())
+ c.commandParamsSchemas["Debugger.stepInto"] = abxjsonschema.SchemaFor[DebuggerStepIntoParams]()
+ c.commandResultSchemas["Debugger.stepInto"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerStepIntoResult]())
+ c.commandParamsSchemas["Debugger.stepOut"] = abxjsonschema.SchemaFor[DebuggerStepOutParams]()
+ c.commandResultSchemas["Debugger.stepOut"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerStepOutResult]())
+ c.commandParamsSchemas["Debugger.stepOver"] = abxjsonschema.SchemaFor[DebuggerStepOverParams]()
+ c.commandResultSchemas["Debugger.stepOver"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerStepOverResult]())
+ c.commandParamsSchemas["DeviceAccess.enable"] = abxjsonschema.SchemaFor[DeviceAccessEnableParams]()
+ c.commandResultSchemas["DeviceAccess.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessEnableResult]())
+ c.commandParamsSchemas["DeviceAccess.disable"] = abxjsonschema.SchemaFor[DeviceAccessDisableParams]()
+ c.commandResultSchemas["DeviceAccess.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessDisableResult]())
+ c.commandParamsSchemas["DeviceAccess.selectPrompt"] = abxjsonschema.SchemaFor[DeviceAccessSelectPromptParams]()
+ c.commandResultSchemas["DeviceAccess.selectPrompt"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessSelectPromptResult]())
+ c.commandParamsSchemas["DeviceAccess.cancelPrompt"] = abxjsonschema.SchemaFor[DeviceAccessCancelPromptParams]()
+ c.commandResultSchemas["DeviceAccess.cancelPrompt"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessCancelPromptResult]())
+ c.commandParamsSchemas["DeviceOrientation.clearDeviceOrientationOverride"] = abxjsonschema.SchemaFor[DeviceOrientationClearDeviceOrientationOverrideParams]()
+ c.commandResultSchemas["DeviceOrientation.clearDeviceOrientationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceOrientationClearDeviceOrientationOverrideResult]())
+ c.commandParamsSchemas["DeviceOrientation.setDeviceOrientationOverride"] = abxjsonschema.SchemaFor[DeviceOrientationSetDeviceOrientationOverrideParams]()
+ c.commandResultSchemas["DeviceOrientation.setDeviceOrientationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceOrientationSetDeviceOrientationOverrideResult]())
+ c.commandParamsSchemas["Emulation.canEmulate"] = abxjsonschema.SchemaFor[EmulationCanEmulateParams]()
+ c.commandResultSchemas["Emulation.canEmulate"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationCanEmulateResult]())
+ c.commandParamsSchemas["Emulation.clearDeviceMetricsOverride"] = abxjsonschema.SchemaFor[EmulationClearDeviceMetricsOverrideParams]()
+ c.commandResultSchemas["Emulation.clearDeviceMetricsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearDeviceMetricsOverrideResult]())
+ c.commandParamsSchemas["Emulation.clearGeolocationOverride"] = abxjsonschema.SchemaFor[EmulationClearGeolocationOverrideParams]()
+ c.commandResultSchemas["Emulation.clearGeolocationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearGeolocationOverrideResult]())
+ c.commandParamsSchemas["Emulation.resetPageScaleFactor"] = abxjsonschema.SchemaFor[EmulationResetPageScaleFactorParams]()
+ c.commandResultSchemas["Emulation.resetPageScaleFactor"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationResetPageScaleFactorResult]())
+ c.commandParamsSchemas["Emulation.setFocusEmulationEnabled"] = abxjsonschema.SchemaFor[EmulationSetFocusEmulationEnabledParams]()
+ c.commandResultSchemas["Emulation.setFocusEmulationEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetFocusEmulationEnabledResult]())
+ c.commandParamsSchemas["Emulation.setAutoDarkModeOverride"] = abxjsonschema.SchemaFor[EmulationSetAutoDarkModeOverrideParams]()
+ c.commandResultSchemas["Emulation.setAutoDarkModeOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetAutoDarkModeOverrideResult]())
+ c.commandParamsSchemas["Emulation.setCPUThrottlingRate"] = abxjsonschema.SchemaFor[EmulationSetCPUThrottlingRateParams]()
+ c.commandResultSchemas["Emulation.setCPUThrottlingRate"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetCPUThrottlingRateResult]())
+ c.commandParamsSchemas["Emulation.setDefaultBackgroundColorOverride"] = abxjsonschema.SchemaFor[EmulationSetDefaultBackgroundColorOverrideParams]()
+ c.commandResultSchemas["Emulation.setDefaultBackgroundColorOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDefaultBackgroundColorOverrideResult]())
+ c.commandParamsSchemas["Emulation.setSafeAreaInsetsOverride"] = abxjsonschema.SchemaFor[EmulationSetSafeAreaInsetsOverrideParams]()
+ c.commandResultSchemas["Emulation.setSafeAreaInsetsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetSafeAreaInsetsOverrideResult]())
+ c.commandParamsSchemas["Emulation.setDeviceMetricsOverride"] = abxjsonschema.SchemaFor[EmulationSetDeviceMetricsOverrideParams]()
+ c.commandResultSchemas["Emulation.setDeviceMetricsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDeviceMetricsOverrideResult]())
+ c.commandParamsSchemas["Emulation.setDevicePostureOverride"] = abxjsonschema.SchemaFor[EmulationSetDevicePostureOverrideParams]()
+ c.commandResultSchemas["Emulation.setDevicePostureOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDevicePostureOverrideResult]())
+ c.commandParamsSchemas["Emulation.clearDevicePostureOverride"] = abxjsonschema.SchemaFor[EmulationClearDevicePostureOverrideParams]()
+ c.commandResultSchemas["Emulation.clearDevicePostureOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearDevicePostureOverrideResult]())
+ c.commandParamsSchemas["Emulation.setDisplayFeaturesOverride"] = abxjsonschema.SchemaFor[EmulationSetDisplayFeaturesOverrideParams]()
+ c.commandResultSchemas["Emulation.setDisplayFeaturesOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDisplayFeaturesOverrideResult]())
+ c.commandParamsSchemas["Emulation.clearDisplayFeaturesOverride"] = abxjsonschema.SchemaFor[EmulationClearDisplayFeaturesOverrideParams]()
+ c.commandResultSchemas["Emulation.clearDisplayFeaturesOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearDisplayFeaturesOverrideResult]())
+ c.commandParamsSchemas["Emulation.setScrollbarsHidden"] = abxjsonschema.SchemaFor[EmulationSetScrollbarsHiddenParams]()
+ c.commandResultSchemas["Emulation.setScrollbarsHidden"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetScrollbarsHiddenResult]())
+ c.commandParamsSchemas["Emulation.setDocumentCookieDisabled"] = abxjsonschema.SchemaFor[EmulationSetDocumentCookieDisabledParams]()
+ c.commandResultSchemas["Emulation.setDocumentCookieDisabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDocumentCookieDisabledResult]())
+ c.commandParamsSchemas["Emulation.setEmitTouchEventsForMouse"] = abxjsonschema.SchemaFor[EmulationSetEmitTouchEventsForMouseParams]()
+ c.commandResultSchemas["Emulation.setEmitTouchEventsForMouse"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetEmitTouchEventsForMouseResult]())
+ c.commandParamsSchemas["Emulation.setEmulatedMedia"] = abxjsonschema.SchemaFor[EmulationSetEmulatedMediaParams]()
+ c.commandResultSchemas["Emulation.setEmulatedMedia"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetEmulatedMediaResult]())
+ c.commandParamsSchemas["Emulation.setEmulatedVisionDeficiency"] = abxjsonschema.SchemaFor[EmulationSetEmulatedVisionDeficiencyParams]()
+ c.commandResultSchemas["Emulation.setEmulatedVisionDeficiency"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetEmulatedVisionDeficiencyResult]())
+ c.commandParamsSchemas["Emulation.setEmulatedOSTextScale"] = abxjsonschema.SchemaFor[EmulationSetEmulatedOSTextScaleParams]()
+ c.commandResultSchemas["Emulation.setEmulatedOSTextScale"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetEmulatedOSTextScaleResult]())
+ c.commandParamsSchemas["Emulation.setGeolocationOverride"] = abxjsonschema.SchemaFor[EmulationSetGeolocationOverrideParams]()
+ c.commandResultSchemas["Emulation.setGeolocationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetGeolocationOverrideResult]())
+ c.commandParamsSchemas["Emulation.getOverriddenSensorInformation"] = abxjsonschema.SchemaFor[EmulationGetOverriddenSensorInformationParams]()
+ c.commandResultSchemas["Emulation.getOverriddenSensorInformation"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationGetOverriddenSensorInformationResult]())
+ c.commandParamsSchemas["Emulation.setSensorOverrideEnabled"] = abxjsonschema.SchemaFor[EmulationSetSensorOverrideEnabledParams]()
+ c.commandResultSchemas["Emulation.setSensorOverrideEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetSensorOverrideEnabledResult]())
+ c.commandParamsSchemas["Emulation.setSensorOverrideReadings"] = abxjsonschema.SchemaFor[EmulationSetSensorOverrideReadingsParams]()
+ c.commandResultSchemas["Emulation.setSensorOverrideReadings"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetSensorOverrideReadingsResult]())
+ c.commandParamsSchemas["Emulation.setPressureSourceOverrideEnabled"] = abxjsonschema.SchemaFor[EmulationSetPressureSourceOverrideEnabledParams]()
+ c.commandResultSchemas["Emulation.setPressureSourceOverrideEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPressureSourceOverrideEnabledResult]())
+ c.commandParamsSchemas["Emulation.setPressureStateOverride"] = abxjsonschema.SchemaFor[EmulationSetPressureStateOverrideParams]()
+ c.commandResultSchemas["Emulation.setPressureStateOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPressureStateOverrideResult]())
+ c.commandParamsSchemas["Emulation.setPressureDataOverride"] = abxjsonschema.SchemaFor[EmulationSetPressureDataOverrideParams]()
+ c.commandResultSchemas["Emulation.setPressureDataOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPressureDataOverrideResult]())
+ c.commandParamsSchemas["Emulation.setIdleOverride"] = abxjsonschema.SchemaFor[EmulationSetIdleOverrideParams]()
+ c.commandResultSchemas["Emulation.setIdleOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetIdleOverrideResult]())
+ c.commandParamsSchemas["Emulation.clearIdleOverride"] = abxjsonschema.SchemaFor[EmulationClearIdleOverrideParams]()
+ c.commandResultSchemas["Emulation.clearIdleOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearIdleOverrideResult]())
+ c.commandParamsSchemas["Emulation.setNavigatorOverrides"] = abxjsonschema.SchemaFor[EmulationSetNavigatorOverridesParams]()
+ c.commandResultSchemas["Emulation.setNavigatorOverrides"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetNavigatorOverridesResult]())
+ c.commandParamsSchemas["Emulation.setPageScaleFactor"] = abxjsonschema.SchemaFor[EmulationSetPageScaleFactorParams]()
+ c.commandResultSchemas["Emulation.setPageScaleFactor"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPageScaleFactorResult]())
+ c.commandParamsSchemas["Emulation.setScriptExecutionDisabled"] = abxjsonschema.SchemaFor[EmulationSetScriptExecutionDisabledParams]()
+ c.commandResultSchemas["Emulation.setScriptExecutionDisabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetScriptExecutionDisabledResult]())
+ c.commandParamsSchemas["Emulation.setTouchEmulationEnabled"] = abxjsonschema.SchemaFor[EmulationSetTouchEmulationEnabledParams]()
+ c.commandResultSchemas["Emulation.setTouchEmulationEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetTouchEmulationEnabledResult]())
+ c.commandParamsSchemas["Emulation.setVirtualTimePolicy"] = abxjsonschema.SchemaFor[EmulationSetVirtualTimePolicyParams]()
+ c.commandResultSchemas["Emulation.setVirtualTimePolicy"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetVirtualTimePolicyResult]())
+ c.commandParamsSchemas["Emulation.setLocaleOverride"] = abxjsonschema.SchemaFor[EmulationSetLocaleOverrideParams]()
+ c.commandResultSchemas["Emulation.setLocaleOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetLocaleOverrideResult]())
+ c.commandParamsSchemas["Emulation.setTimezoneOverride"] = abxjsonschema.SchemaFor[EmulationSetTimezoneOverrideParams]()
+ c.commandResultSchemas["Emulation.setTimezoneOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetTimezoneOverrideResult]())
+ c.commandParamsSchemas["Emulation.setVisibleSize"] = abxjsonschema.SchemaFor[EmulationSetVisibleSizeParams]()
+ c.commandResultSchemas["Emulation.setVisibleSize"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetVisibleSizeResult]())
+ c.commandParamsSchemas["Emulation.setDisabledImageTypes"] = abxjsonschema.SchemaFor[EmulationSetDisabledImageTypesParams]()
+ c.commandResultSchemas["Emulation.setDisabledImageTypes"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDisabledImageTypesResult]())
+ c.commandParamsSchemas["Emulation.setDataSaverOverride"] = abxjsonschema.SchemaFor[EmulationSetDataSaverOverrideParams]()
+ c.commandResultSchemas["Emulation.setDataSaverOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDataSaverOverrideResult]())
+ c.commandParamsSchemas["Emulation.setHardwareConcurrencyOverride"] = abxjsonschema.SchemaFor[EmulationSetHardwareConcurrencyOverrideParams]()
+ c.commandResultSchemas["Emulation.setHardwareConcurrencyOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetHardwareConcurrencyOverrideResult]())
+ c.commandParamsSchemas["Emulation.setUserAgentOverride"] = abxjsonschema.SchemaFor[EmulationSetUserAgentOverrideParams]()
+ c.commandResultSchemas["Emulation.setUserAgentOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetUserAgentOverrideResult]())
+ c.commandParamsSchemas["Emulation.setAutomationOverride"] = abxjsonschema.SchemaFor[EmulationSetAutomationOverrideParams]()
+ c.commandResultSchemas["Emulation.setAutomationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetAutomationOverrideResult]())
+ c.commandParamsSchemas["Emulation.setSmallViewportHeightDifferenceOverride"] = abxjsonschema.SchemaFor[EmulationSetSmallViewportHeightDifferenceOverrideParams]()
+ c.commandResultSchemas["Emulation.setSmallViewportHeightDifferenceOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetSmallViewportHeightDifferenceOverrideResult]())
+ c.commandParamsSchemas["Emulation.getScreenInfos"] = abxjsonschema.SchemaFor[EmulationGetScreenInfosParams]()
+ c.commandResultSchemas["Emulation.getScreenInfos"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationGetScreenInfosResult]())
+ c.commandParamsSchemas["Emulation.addScreen"] = abxjsonschema.SchemaFor[EmulationAddScreenParams]()
+ c.commandResultSchemas["Emulation.addScreen"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationAddScreenResult]())
+ c.commandParamsSchemas["Emulation.updateScreen"] = abxjsonschema.SchemaFor[EmulationUpdateScreenParams]()
+ c.commandResultSchemas["Emulation.updateScreen"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationUpdateScreenResult]())
+ c.commandParamsSchemas["Emulation.removeScreen"] = abxjsonschema.SchemaFor[EmulationRemoveScreenParams]()
+ c.commandResultSchemas["Emulation.removeScreen"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationRemoveScreenResult]())
+ c.commandParamsSchemas["Emulation.setPrimaryScreen"] = abxjsonschema.SchemaFor[EmulationSetPrimaryScreenParams]()
+ c.commandResultSchemas["Emulation.setPrimaryScreen"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPrimaryScreenResult]())
+ c.commandParamsSchemas["EventBreakpoints.setInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[EventBreakpointsSetInstrumentationBreakpointParams]()
+ c.commandResultSchemas["EventBreakpoints.setInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[EventBreakpointsSetInstrumentationBreakpointResult]())
+ c.commandParamsSchemas["EventBreakpoints.removeInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[EventBreakpointsRemoveInstrumentationBreakpointParams]()
+ c.commandResultSchemas["EventBreakpoints.removeInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[EventBreakpointsRemoveInstrumentationBreakpointResult]())
+ c.commandParamsSchemas["EventBreakpoints.disable"] = abxjsonschema.SchemaFor[EventBreakpointsDisableParams]()
+ c.commandResultSchemas["EventBreakpoints.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[EventBreakpointsDisableResult]())
+ c.commandParamsSchemas["Extensions.triggerAction"] = abxjsonschema.SchemaFor[ExtensionsTriggerActionParams]()
+ c.commandResultSchemas["Extensions.triggerAction"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsTriggerActionResult]())
+ c.commandParamsSchemas["Extensions.loadUnpacked"] = abxjsonschema.SchemaFor[ExtensionsLoadUnpackedParams]()
+ c.commandResultSchemas["Extensions.loadUnpacked"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsLoadUnpackedResult]())
+ c.commandParamsSchemas["Extensions.getExtensions"] = abxjsonschema.SchemaFor[ExtensionsGetExtensionsParams]()
+ c.commandResultSchemas["Extensions.getExtensions"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsGetExtensionsResult]())
+ c.commandParamsSchemas["Extensions.uninstall"] = abxjsonschema.SchemaFor[ExtensionsUninstallParams]()
+ c.commandResultSchemas["Extensions.uninstall"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsUninstallResult]())
+ c.commandParamsSchemas["Extensions.getStorageItems"] = abxjsonschema.SchemaFor[ExtensionsGetStorageItemsParams]()
+ c.commandResultSchemas["Extensions.getStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsGetStorageItemsResult]())
+ c.commandParamsSchemas["Extensions.removeStorageItems"] = abxjsonschema.SchemaFor[ExtensionsRemoveStorageItemsParams]()
+ c.commandResultSchemas["Extensions.removeStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsRemoveStorageItemsResult]())
+ c.commandParamsSchemas["Extensions.clearStorageItems"] = abxjsonschema.SchemaFor[ExtensionsClearStorageItemsParams]()
+ c.commandResultSchemas["Extensions.clearStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsClearStorageItemsResult]())
+ c.commandParamsSchemas["Extensions.setStorageItems"] = abxjsonschema.SchemaFor[ExtensionsSetStorageItemsParams]()
+ c.commandResultSchemas["Extensions.setStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsSetStorageItemsResult]())
+ c.commandParamsSchemas["FedCm.enable"] = abxjsonschema.SchemaFor[FedCmEnableParams]()
+ c.commandResultSchemas["FedCm.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmEnableResult]())
+ c.commandParamsSchemas["FedCm.disable"] = abxjsonschema.SchemaFor[FedCmDisableParams]()
+ c.commandResultSchemas["FedCm.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmDisableResult]())
+ c.commandParamsSchemas["FedCm.selectAccount"] = abxjsonschema.SchemaFor[FedCmSelectAccountParams]()
+ c.commandResultSchemas["FedCm.selectAccount"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmSelectAccountResult]())
+ c.commandParamsSchemas["FedCm.clickDialogButton"] = abxjsonschema.SchemaFor[FedCmClickDialogButtonParams]()
+ c.commandResultSchemas["FedCm.clickDialogButton"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmClickDialogButtonResult]())
+ c.commandParamsSchemas["FedCm.openUrl"] = abxjsonschema.SchemaFor[FedCmOpenURLParams]()
+ c.commandResultSchemas["FedCm.openUrl"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmOpenURLResult]())
+ c.commandParamsSchemas["FedCm.dismissDialog"] = abxjsonschema.SchemaFor[FedCmDismissDialogParams]()
+ c.commandResultSchemas["FedCm.dismissDialog"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmDismissDialogResult]())
+ c.commandParamsSchemas["FedCm.resetCooldown"] = abxjsonschema.SchemaFor[FedCmResetCooldownParams]()
+ c.commandResultSchemas["FedCm.resetCooldown"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmResetCooldownResult]())
+ c.commandParamsSchemas["Fetch.disable"] = abxjsonschema.SchemaFor[FetchDisableParams]()
+ c.commandResultSchemas["Fetch.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchDisableResult]())
+ c.commandParamsSchemas["Fetch.enable"] = abxjsonschema.SchemaFor[FetchEnableParams]()
+ c.commandResultSchemas["Fetch.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchEnableResult]())
+ c.commandParamsSchemas["Fetch.failRequest"] = abxjsonschema.SchemaFor[FetchFailRequestParams]()
+ c.commandResultSchemas["Fetch.failRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchFailRequestResult]())
+ c.commandParamsSchemas["Fetch.fulfillRequest"] = abxjsonschema.SchemaFor[FetchFulfillRequestParams]()
+ c.commandResultSchemas["Fetch.fulfillRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchFulfillRequestResult]())
+ c.commandParamsSchemas["Fetch.continueRequest"] = abxjsonschema.SchemaFor[FetchContinueRequestParams]()
+ c.commandResultSchemas["Fetch.continueRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchContinueRequestResult]())
+ c.commandParamsSchemas["Fetch.continueWithAuth"] = abxjsonschema.SchemaFor[FetchContinueWithAuthParams]()
+ c.commandResultSchemas["Fetch.continueWithAuth"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchContinueWithAuthResult]())
+ c.commandParamsSchemas["Fetch.continueResponse"] = abxjsonschema.SchemaFor[FetchContinueResponseParams]()
+ c.commandResultSchemas["Fetch.continueResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchContinueResponseResult]())
+ c.commandParamsSchemas["Fetch.getResponseBody"] = abxjsonschema.SchemaFor[FetchGetResponseBodyParams]()
+ c.commandResultSchemas["Fetch.getResponseBody"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchGetResponseBodyResult]())
+ c.commandParamsSchemas["Fetch.takeResponseBodyAsStream"] = abxjsonschema.SchemaFor[FetchTakeResponseBodyAsStreamParams]()
+ c.commandResultSchemas["Fetch.takeResponseBodyAsStream"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchTakeResponseBodyAsStreamResult]())
+ c.commandParamsSchemas["FileSystem.getDirectory"] = abxjsonschema.SchemaFor[FileSystemGetDirectoryParams]()
+ c.commandResultSchemas["FileSystem.getDirectory"] = nativeResultSchema(abxjsonschema.SchemaFor[FileSystemGetDirectoryResult]())
+ c.commandParamsSchemas["HeadlessExperimental.beginFrame"] = abxjsonschema.SchemaFor[HeadlessExperimentalBeginFrameParams]()
+ c.commandResultSchemas["HeadlessExperimental.beginFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[HeadlessExperimentalBeginFrameResult]())
+ c.commandParamsSchemas["HeadlessExperimental.disable"] = abxjsonschema.SchemaFor[HeadlessExperimentalDisableParams]()
+ c.commandResultSchemas["HeadlessExperimental.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[HeadlessExperimentalDisableResult]())
+ c.commandParamsSchemas["HeadlessExperimental.enable"] = abxjsonschema.SchemaFor[HeadlessExperimentalEnableParams]()
+ c.commandResultSchemas["HeadlessExperimental.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[HeadlessExperimentalEnableResult]())
+ c.commandParamsSchemas["HeapProfiler.addInspectedHeapObject"] = abxjsonschema.SchemaFor[HeapProfilerAddInspectedHeapObjectParams]()
+ c.commandResultSchemas["HeapProfiler.addInspectedHeapObject"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerAddInspectedHeapObjectResult]())
+ c.commandParamsSchemas["HeapProfiler.collectGarbage"] = abxjsonschema.SchemaFor[HeapProfilerCollectGarbageParams]()
+ c.commandResultSchemas["HeapProfiler.collectGarbage"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerCollectGarbageResult]())
+ c.commandParamsSchemas["HeapProfiler.disable"] = abxjsonschema.SchemaFor[HeapProfilerDisableParams]()
+ c.commandResultSchemas["HeapProfiler.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerDisableResult]())
+ c.commandParamsSchemas["HeapProfiler.enable"] = abxjsonschema.SchemaFor[HeapProfilerEnableParams]()
+ c.commandResultSchemas["HeapProfiler.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerEnableResult]())
+ c.commandParamsSchemas["HeapProfiler.getHeapObjectId"] = abxjsonschema.SchemaFor[HeapProfilerGetHeapObjectIDParams]()
+ c.commandResultSchemas["HeapProfiler.getHeapObjectId"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerGetHeapObjectIDResult]())
+ c.commandParamsSchemas["HeapProfiler.getObjectByHeapObjectId"] = abxjsonschema.SchemaFor[HeapProfilerGetObjectByHeapObjectIDParams]()
+ c.commandResultSchemas["HeapProfiler.getObjectByHeapObjectId"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerGetObjectByHeapObjectIDResult]())
+ c.commandParamsSchemas["HeapProfiler.getSamplingProfile"] = abxjsonschema.SchemaFor[HeapProfilerGetSamplingProfileParams]()
+ c.commandResultSchemas["HeapProfiler.getSamplingProfile"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerGetSamplingProfileResult]())
+ c.commandParamsSchemas["HeapProfiler.startSampling"] = abxjsonschema.SchemaFor[HeapProfilerStartSamplingParams]()
+ c.commandResultSchemas["HeapProfiler.startSampling"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerStartSamplingResult]())
+ c.commandParamsSchemas["HeapProfiler.startTrackingHeapObjects"] = abxjsonschema.SchemaFor[HeapProfilerStartTrackingHeapObjectsParams]()
+ c.commandResultSchemas["HeapProfiler.startTrackingHeapObjects"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerStartTrackingHeapObjectsResult]())
+ c.commandParamsSchemas["HeapProfiler.stopSampling"] = abxjsonschema.SchemaFor[HeapProfilerStopSamplingParams]()
+ c.commandResultSchemas["HeapProfiler.stopSampling"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerStopSamplingResult]())
+ c.commandParamsSchemas["HeapProfiler.stopTrackingHeapObjects"] = abxjsonschema.SchemaFor[HeapProfilerStopTrackingHeapObjectsParams]()
+ c.commandResultSchemas["HeapProfiler.stopTrackingHeapObjects"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerStopTrackingHeapObjectsResult]())
+ c.commandParamsSchemas["HeapProfiler.takeHeapSnapshot"] = abxjsonschema.SchemaFor[HeapProfilerTakeHeapSnapshotParams]()
+ c.commandResultSchemas["HeapProfiler.takeHeapSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerTakeHeapSnapshotResult]())
+ c.commandParamsSchemas["IO.close"] = abxjsonschema.SchemaFor[IOCloseParams]()
+ c.commandResultSchemas["IO.close"] = nativeResultSchema(abxjsonschema.SchemaFor[IOCloseResult]())
+ c.commandParamsSchemas["IO.read"] = abxjsonschema.SchemaFor[IOReadParams]()
+ c.commandResultSchemas["IO.read"] = nativeResultSchema(abxjsonschema.SchemaFor[IOReadResult]())
+ c.commandParamsSchemas["IO.resolveBlob"] = abxjsonschema.SchemaFor[IOResolveBlobParams]()
+ c.commandResultSchemas["IO.resolveBlob"] = nativeResultSchema(abxjsonschema.SchemaFor[IOResolveBlobResult]())
+ c.commandParamsSchemas["IndexedDB.clearObjectStore"] = abxjsonschema.SchemaFor[IndexedDBClearObjectStoreParams]()
+ c.commandResultSchemas["IndexedDB.clearObjectStore"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBClearObjectStoreResult]())
+ c.commandParamsSchemas["IndexedDB.deleteDatabase"] = abxjsonschema.SchemaFor[IndexedDBDeleteDatabaseParams]()
+ c.commandResultSchemas["IndexedDB.deleteDatabase"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBDeleteDatabaseResult]())
+ c.commandParamsSchemas["IndexedDB.deleteObjectStoreEntries"] = abxjsonschema.SchemaFor[IndexedDBDeleteObjectStoreEntriesParams]()
+ c.commandResultSchemas["IndexedDB.deleteObjectStoreEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBDeleteObjectStoreEntriesResult]())
+ c.commandParamsSchemas["IndexedDB.disable"] = abxjsonschema.SchemaFor[IndexedDBDisableParams]()
+ c.commandResultSchemas["IndexedDB.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBDisableResult]())
+ c.commandParamsSchemas["IndexedDB.enable"] = abxjsonschema.SchemaFor[IndexedDBEnableParams]()
+ c.commandResultSchemas["IndexedDB.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBEnableResult]())
+ c.commandParamsSchemas["IndexedDB.requestData"] = abxjsonschema.SchemaFor[IndexedDBRequestDataParams]()
+ c.commandResultSchemas["IndexedDB.requestData"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBRequestDataResult]())
+ c.commandParamsSchemas["IndexedDB.getMetadata"] = abxjsonschema.SchemaFor[IndexedDBGetMetadataParams]()
+ c.commandResultSchemas["IndexedDB.getMetadata"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBGetMetadataResult]())
+ c.commandParamsSchemas["IndexedDB.requestDatabase"] = abxjsonschema.SchemaFor[IndexedDBRequestDatabaseParams]()
+ c.commandResultSchemas["IndexedDB.requestDatabase"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBRequestDatabaseResult]())
+ c.commandParamsSchemas["IndexedDB.requestDatabaseNames"] = abxjsonschema.SchemaFor[IndexedDBRequestDatabaseNamesParams]()
+ c.commandResultSchemas["IndexedDB.requestDatabaseNames"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBRequestDatabaseNamesResult]())
+ c.commandParamsSchemas["Input.dispatchDragEvent"] = abxjsonschema.SchemaFor[InputDispatchDragEventParams]()
+ c.commandResultSchemas["Input.dispatchDragEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDispatchDragEventResult]())
+ c.commandParamsSchemas["Input.dispatchKeyEvent"] = abxjsonschema.SchemaFor[InputDispatchKeyEventParams]()
+ c.commandResultSchemas["Input.dispatchKeyEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDispatchKeyEventResult]())
+ c.commandParamsSchemas["Input.insertText"] = abxjsonschema.SchemaFor[InputInsertTextParams]()
+ c.commandResultSchemas["Input.insertText"] = nativeResultSchema(abxjsonschema.SchemaFor[InputInsertTextResult]())
+ c.commandParamsSchemas["Input.imeSetComposition"] = abxjsonschema.SchemaFor[InputImeSetCompositionParams]()
+ c.commandResultSchemas["Input.imeSetComposition"] = nativeResultSchema(abxjsonschema.SchemaFor[InputImeSetCompositionResult]())
+ c.commandParamsSchemas["Input.dispatchMouseEvent"] = abxjsonschema.SchemaFor[InputDispatchMouseEventParams]()
+ c.commandResultSchemas["Input.dispatchMouseEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDispatchMouseEventResult]())
+ c.commandParamsSchemas["Input.dispatchTouchEvent"] = abxjsonschema.SchemaFor[InputDispatchTouchEventParams]()
+ c.commandResultSchemas["Input.dispatchTouchEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDispatchTouchEventResult]())
+ c.commandParamsSchemas["Input.cancelDragging"] = abxjsonschema.SchemaFor[InputCancelDraggingParams]()
+ c.commandResultSchemas["Input.cancelDragging"] = nativeResultSchema(abxjsonschema.SchemaFor[InputCancelDraggingResult]())
+ c.commandParamsSchemas["Input.emulateTouchFromMouseEvent"] = abxjsonschema.SchemaFor[InputEmulateTouchFromMouseEventParams]()
+ c.commandResultSchemas["Input.emulateTouchFromMouseEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputEmulateTouchFromMouseEventResult]())
+ c.commandParamsSchemas["Input.setIgnoreInputEvents"] = abxjsonschema.SchemaFor[InputSetIgnoreInputEventsParams]()
+ c.commandResultSchemas["Input.setIgnoreInputEvents"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSetIgnoreInputEventsResult]())
+ c.commandParamsSchemas["Input.setInterceptDrags"] = abxjsonschema.SchemaFor[InputSetInterceptDragsParams]()
+ c.commandResultSchemas["Input.setInterceptDrags"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSetInterceptDragsResult]())
+ c.commandParamsSchemas["Input.synthesizePinchGesture"] = abxjsonschema.SchemaFor[InputSynthesizePinchGestureParams]()
+ c.commandResultSchemas["Input.synthesizePinchGesture"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSynthesizePinchGestureResult]())
+ c.commandParamsSchemas["Input.synthesizeScrollGesture"] = abxjsonschema.SchemaFor[InputSynthesizeScrollGestureParams]()
+ c.commandResultSchemas["Input.synthesizeScrollGesture"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSynthesizeScrollGestureResult]())
+ c.commandParamsSchemas["Input.synthesizeTapGesture"] = abxjsonschema.SchemaFor[InputSynthesizeTapGestureParams]()
+ c.commandResultSchemas["Input.synthesizeTapGesture"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSynthesizeTapGestureResult]())
+ c.commandParamsSchemas["Inspector.disable"] = abxjsonschema.SchemaFor[InspectorDisableParams]()
+ c.commandResultSchemas["Inspector.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorDisableResult]())
+ c.commandParamsSchemas["Inspector.enable"] = abxjsonschema.SchemaFor[InspectorEnableParams]()
+ c.commandResultSchemas["Inspector.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorEnableResult]())
+ c.commandParamsSchemas["LayerTree.compositingReasons"] = abxjsonschema.SchemaFor[LayerTreeCompositingReasonsParams]()
+ c.commandResultSchemas["LayerTree.compositingReasons"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeCompositingReasonsResult]())
+ c.commandParamsSchemas["LayerTree.disable"] = abxjsonschema.SchemaFor[LayerTreeDisableParams]()
+ c.commandResultSchemas["LayerTree.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeDisableResult]())
+ c.commandParamsSchemas["LayerTree.enable"] = abxjsonschema.SchemaFor[LayerTreeEnableParams]()
+ c.commandResultSchemas["LayerTree.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeEnableResult]())
+ c.commandParamsSchemas["LayerTree.loadSnapshot"] = abxjsonschema.SchemaFor[LayerTreeLoadSnapshotParams]()
+ c.commandResultSchemas["LayerTree.loadSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeLoadSnapshotResult]())
+ c.commandParamsSchemas["LayerTree.makeSnapshot"] = abxjsonschema.SchemaFor[LayerTreeMakeSnapshotParams]()
+ c.commandResultSchemas["LayerTree.makeSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeMakeSnapshotResult]())
+ c.commandParamsSchemas["LayerTree.profileSnapshot"] = abxjsonschema.SchemaFor[LayerTreeProfileSnapshotParams]()
+ c.commandResultSchemas["LayerTree.profileSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeProfileSnapshotResult]())
+ c.commandParamsSchemas["LayerTree.releaseSnapshot"] = abxjsonschema.SchemaFor[LayerTreeReleaseSnapshotParams]()
+ c.commandResultSchemas["LayerTree.releaseSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeReleaseSnapshotResult]())
+ c.commandParamsSchemas["LayerTree.replaySnapshot"] = abxjsonschema.SchemaFor[LayerTreeReplaySnapshotParams]()
+ c.commandResultSchemas["LayerTree.replaySnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeReplaySnapshotResult]())
+ c.commandParamsSchemas["LayerTree.snapshotCommandLog"] = abxjsonschema.SchemaFor[LayerTreeSnapshotCommandLogParams]()
+ c.commandResultSchemas["LayerTree.snapshotCommandLog"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeSnapshotCommandLogResult]())
+ c.commandParamsSchemas["Log.clear"] = abxjsonschema.SchemaFor[LogClearParams]()
+ c.commandResultSchemas["Log.clear"] = nativeResultSchema(abxjsonschema.SchemaFor[LogClearResult]())
+ c.commandParamsSchemas["Log.disable"] = abxjsonschema.SchemaFor[LogDisableParams]()
+ c.commandResultSchemas["Log.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[LogDisableResult]())
+ c.commandParamsSchemas["Log.enable"] = abxjsonschema.SchemaFor[LogEnableParams]()
+ c.commandResultSchemas["Log.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[LogEnableResult]())
+ c.commandParamsSchemas["Log.startViolationsReport"] = abxjsonschema.SchemaFor[LogStartViolationsReportParams]()
+ c.commandResultSchemas["Log.startViolationsReport"] = nativeResultSchema(abxjsonschema.SchemaFor[LogStartViolationsReportResult]())
+ c.commandParamsSchemas["Log.stopViolationsReport"] = abxjsonschema.SchemaFor[LogStopViolationsReportParams]()
+ c.commandResultSchemas["Log.stopViolationsReport"] = nativeResultSchema(abxjsonschema.SchemaFor[LogStopViolationsReportResult]())
+ c.commandParamsSchemas["Media.enable"] = abxjsonschema.SchemaFor[MediaEnableParams]()
+ c.commandResultSchemas["Media.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaEnableResult]())
+ c.commandParamsSchemas["Media.disable"] = abxjsonschema.SchemaFor[MediaDisableParams]()
+ c.commandResultSchemas["Media.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaDisableResult]())
+ c.commandParamsSchemas["Memory.getDOMCounters"] = abxjsonschema.SchemaFor[MemoryGetDOMCountersParams]()
+ c.commandResultSchemas["Memory.getDOMCounters"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetDOMCountersResult]())
+ c.commandParamsSchemas["Memory.getDOMCountersForLeakDetection"] = abxjsonschema.SchemaFor[MemoryGetDOMCountersForLeakDetectionParams]()
+ c.commandResultSchemas["Memory.getDOMCountersForLeakDetection"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetDOMCountersForLeakDetectionResult]())
+ c.commandParamsSchemas["Memory.prepareForLeakDetection"] = abxjsonschema.SchemaFor[MemoryPrepareForLeakDetectionParams]()
+ c.commandResultSchemas["Memory.prepareForLeakDetection"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryPrepareForLeakDetectionResult]())
+ c.commandParamsSchemas["Memory.forciblyPurgeJavaScriptMemory"] = abxjsonschema.SchemaFor[MemoryForciblyPurgeJavaScriptMemoryParams]()
+ c.commandResultSchemas["Memory.forciblyPurgeJavaScriptMemory"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryForciblyPurgeJavaScriptMemoryResult]())
+ c.commandParamsSchemas["Memory.setPressureNotificationsSuppressed"] = abxjsonschema.SchemaFor[MemorySetPressureNotificationsSuppressedParams]()
+ c.commandResultSchemas["Memory.setPressureNotificationsSuppressed"] = nativeResultSchema(abxjsonschema.SchemaFor[MemorySetPressureNotificationsSuppressedResult]())
+ c.commandParamsSchemas["Memory.simulatePressureNotification"] = abxjsonschema.SchemaFor[MemorySimulatePressureNotificationParams]()
+ c.commandResultSchemas["Memory.simulatePressureNotification"] = nativeResultSchema(abxjsonschema.SchemaFor[MemorySimulatePressureNotificationResult]())
+ c.commandParamsSchemas["Memory.startSampling"] = abxjsonschema.SchemaFor[MemoryStartSamplingParams]()
+ c.commandResultSchemas["Memory.startSampling"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryStartSamplingResult]())
+ c.commandParamsSchemas["Memory.stopSampling"] = abxjsonschema.SchemaFor[MemoryStopSamplingParams]()
+ c.commandResultSchemas["Memory.stopSampling"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryStopSamplingResult]())
+ c.commandParamsSchemas["Memory.getAllTimeSamplingProfile"] = abxjsonschema.SchemaFor[MemoryGetAllTimeSamplingProfileParams]()
+ c.commandResultSchemas["Memory.getAllTimeSamplingProfile"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetAllTimeSamplingProfileResult]())
+ c.commandParamsSchemas["Memory.getBrowserSamplingProfile"] = abxjsonschema.SchemaFor[MemoryGetBrowserSamplingProfileParams]()
+ c.commandResultSchemas["Memory.getBrowserSamplingProfile"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetBrowserSamplingProfileResult]())
+ c.commandParamsSchemas["Memory.getSamplingProfile"] = abxjsonschema.SchemaFor[MemoryGetSamplingProfileParams]()
+ c.commandResultSchemas["Memory.getSamplingProfile"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetSamplingProfileResult]())
+ c.commandParamsSchemas["Network.setAcceptedEncodings"] = abxjsonschema.SchemaFor[NetworkSetAcceptedEncodingsParams]()
+ c.commandResultSchemas["Network.setAcceptedEncodings"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetAcceptedEncodingsResult]())
+ c.commandParamsSchemas["Network.clearAcceptedEncodingsOverride"] = abxjsonschema.SchemaFor[NetworkClearAcceptedEncodingsOverrideParams]()
+ c.commandResultSchemas["Network.clearAcceptedEncodingsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkClearAcceptedEncodingsOverrideResult]())
+ c.commandParamsSchemas["Network.canClearBrowserCache"] = abxjsonschema.SchemaFor[NetworkCanClearBrowserCacheParams]()
+ c.commandResultSchemas["Network.canClearBrowserCache"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkCanClearBrowserCacheResult]())
+ c.commandParamsSchemas["Network.canClearBrowserCookies"] = abxjsonschema.SchemaFor[NetworkCanClearBrowserCookiesParams]()
+ c.commandResultSchemas["Network.canClearBrowserCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkCanClearBrowserCookiesResult]())
+ c.commandParamsSchemas["Network.canEmulateNetworkConditions"] = abxjsonschema.SchemaFor[NetworkCanEmulateNetworkConditionsParams]()
+ c.commandResultSchemas["Network.canEmulateNetworkConditions"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkCanEmulateNetworkConditionsResult]())
+ c.commandParamsSchemas["Network.clearBrowserCache"] = abxjsonschema.SchemaFor[NetworkClearBrowserCacheParams]()
+ c.commandResultSchemas["Network.clearBrowserCache"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkClearBrowserCacheResult]())
+ c.commandParamsSchemas["Network.clearBrowserCookies"] = abxjsonschema.SchemaFor[NetworkClearBrowserCookiesParams]()
+ c.commandResultSchemas["Network.clearBrowserCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkClearBrowserCookiesResult]())
+ c.commandParamsSchemas["Network.continueInterceptedRequest"] = abxjsonschema.SchemaFor[NetworkContinueInterceptedRequestParams]()
+ c.commandResultSchemas["Network.continueInterceptedRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkContinueInterceptedRequestResult]())
+ c.commandParamsSchemas["Network.deleteCookies"] = abxjsonschema.SchemaFor[NetworkDeleteCookiesParams]()
+ c.commandResultSchemas["Network.deleteCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDeleteCookiesResult]())
+ c.commandParamsSchemas["Network.disable"] = abxjsonschema.SchemaFor[NetworkDisableParams]()
+ c.commandResultSchemas["Network.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDisableResult]())
+ c.commandParamsSchemas["Network.emulateNetworkConditions"] = abxjsonschema.SchemaFor[NetworkEmulateNetworkConditionsParams]()
+ c.commandResultSchemas["Network.emulateNetworkConditions"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEmulateNetworkConditionsResult]())
+ c.commandParamsSchemas["Network.emulateNetworkConditionsByRule"] = abxjsonschema.SchemaFor[NetworkEmulateNetworkConditionsByRuleParams]()
+ c.commandResultSchemas["Network.emulateNetworkConditionsByRule"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEmulateNetworkConditionsByRuleResult]())
+ c.commandParamsSchemas["Network.overrideNetworkState"] = abxjsonschema.SchemaFor[NetworkOverrideNetworkStateParams]()
+ c.commandResultSchemas["Network.overrideNetworkState"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkOverrideNetworkStateResult]())
+ c.commandParamsSchemas["Network.enable"] = abxjsonschema.SchemaFor[NetworkEnableParams]()
+ c.commandResultSchemas["Network.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEnableResult]())
+ c.commandParamsSchemas["Network.configureDurableMessages"] = abxjsonschema.SchemaFor[NetworkConfigureDurableMessagesParams]()
+ c.commandResultSchemas["Network.configureDurableMessages"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkConfigureDurableMessagesResult]())
+ c.commandParamsSchemas["Network.getAllCookies"] = abxjsonschema.SchemaFor[NetworkGetAllCookiesParams]()
+ c.commandResultSchemas["Network.getAllCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetAllCookiesResult]())
+ c.commandParamsSchemas["Network.getCertificate"] = abxjsonschema.SchemaFor[NetworkGetCertificateParams]()
+ c.commandResultSchemas["Network.getCertificate"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetCertificateResult]())
+ c.commandParamsSchemas["Network.getCookies"] = abxjsonschema.SchemaFor[NetworkGetCookiesParams]()
+ c.commandResultSchemas["Network.getCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetCookiesResult]())
+ c.commandParamsSchemas["Network.getResponseBody"] = abxjsonschema.SchemaFor[NetworkGetResponseBodyParams]()
+ c.commandResultSchemas["Network.getResponseBody"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetResponseBodyResult]())
+ c.commandParamsSchemas["Network.getRequestPostData"] = abxjsonschema.SchemaFor[NetworkGetRequestPostDataParams]()
+ c.commandResultSchemas["Network.getRequestPostData"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetRequestPostDataResult]())
+ c.commandParamsSchemas["Network.getResponseBodyForInterception"] = abxjsonschema.SchemaFor[NetworkGetResponseBodyForInterceptionParams]()
+ c.commandResultSchemas["Network.getResponseBodyForInterception"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetResponseBodyForInterceptionResult]())
+ c.commandParamsSchemas["Network.takeResponseBodyForInterceptionAsStream"] = abxjsonschema.SchemaFor[NetworkTakeResponseBodyForInterceptionAsStreamParams]()
+ c.commandResultSchemas["Network.takeResponseBodyForInterceptionAsStream"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkTakeResponseBodyForInterceptionAsStreamResult]())
+ c.commandParamsSchemas["Network.replayXHR"] = abxjsonschema.SchemaFor[NetworkReplayXHRParams]()
+ c.commandResultSchemas["Network.replayXHR"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkReplayXHRResult]())
+ c.commandParamsSchemas["Network.searchInResponseBody"] = abxjsonschema.SchemaFor[NetworkSearchInResponseBodyParams]()
+ c.commandResultSchemas["Network.searchInResponseBody"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSearchInResponseBodyResult]())
+ c.commandParamsSchemas["Network.setBlockedURLs"] = abxjsonschema.SchemaFor[NetworkSetBlockedURLsParams]()
+ c.commandResultSchemas["Network.setBlockedURLs"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetBlockedURLsResult]())
+ c.commandParamsSchemas["Network.setBypassServiceWorker"] = abxjsonschema.SchemaFor[NetworkSetBypassServiceWorkerParams]()
+ c.commandResultSchemas["Network.setBypassServiceWorker"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetBypassServiceWorkerResult]())
+ c.commandParamsSchemas["Network.setCacheDisabled"] = abxjsonschema.SchemaFor[NetworkSetCacheDisabledParams]()
+ c.commandResultSchemas["Network.setCacheDisabled"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetCacheDisabledResult]())
+ c.commandParamsSchemas["Network.setCookie"] = abxjsonschema.SchemaFor[NetworkSetCookieParams]()
+ c.commandResultSchemas["Network.setCookie"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetCookieResult]())
+ c.commandParamsSchemas["Network.setCookies"] = abxjsonschema.SchemaFor[NetworkSetCookiesParams]()
+ c.commandResultSchemas["Network.setCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetCookiesResult]())
+ c.commandParamsSchemas["Network.setExtraHTTPHeaders"] = abxjsonschema.SchemaFor[NetworkSetExtraHTTPHeadersParams]()
+ c.commandResultSchemas["Network.setExtraHTTPHeaders"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetExtraHTTPHeadersResult]())
+ c.commandParamsSchemas["Network.setAttachDebugStack"] = abxjsonschema.SchemaFor[NetworkSetAttachDebugStackParams]()
+ c.commandResultSchemas["Network.setAttachDebugStack"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetAttachDebugStackResult]())
+ c.commandParamsSchemas["Network.setRequestInterception"] = abxjsonschema.SchemaFor[NetworkSetRequestInterceptionParams]()
+ c.commandResultSchemas["Network.setRequestInterception"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetRequestInterceptionResult]())
+ c.commandParamsSchemas["Network.setUserAgentOverride"] = abxjsonschema.SchemaFor[NetworkSetUserAgentOverrideParams]()
+ c.commandResultSchemas["Network.setUserAgentOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetUserAgentOverrideResult]())
+ c.commandParamsSchemas["Network.streamResourceContent"] = abxjsonschema.SchemaFor[NetworkStreamResourceContentParams]()
+ c.commandResultSchemas["Network.streamResourceContent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkStreamResourceContentResult]())
+ c.commandParamsSchemas["Network.getSecurityIsolationStatus"] = abxjsonschema.SchemaFor[NetworkGetSecurityIsolationStatusParams]()
+ c.commandResultSchemas["Network.getSecurityIsolationStatus"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetSecurityIsolationStatusResult]())
+ c.commandParamsSchemas["Network.enableReportingApi"] = abxjsonschema.SchemaFor[NetworkEnableReportingAPIParams]()
+ c.commandResultSchemas["Network.enableReportingApi"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEnableReportingAPIResult]())
+ c.commandParamsSchemas["Network.enableDeviceBoundSessions"] = abxjsonschema.SchemaFor[NetworkEnableDeviceBoundSessionsParams]()
+ c.commandResultSchemas["Network.enableDeviceBoundSessions"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEnableDeviceBoundSessionsResult]())
+ c.commandParamsSchemas["Network.fetchSchemefulSite"] = abxjsonschema.SchemaFor[NetworkFetchSchemefulSiteParams]()
+ c.commandResultSchemas["Network.fetchSchemefulSite"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkFetchSchemefulSiteResult]())
+ c.commandParamsSchemas["Network.loadNetworkResource"] = abxjsonschema.SchemaFor[NetworkLoadNetworkResourceParams]()
+ c.commandResultSchemas["Network.loadNetworkResource"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkLoadNetworkResourceResult]())
+ c.commandParamsSchemas["Network.setCookieControls"] = abxjsonschema.SchemaFor[NetworkSetCookieControlsParams]()
+ c.commandResultSchemas["Network.setCookieControls"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetCookieControlsResult]())
+ c.commandParamsSchemas["Overlay.disable"] = abxjsonschema.SchemaFor[OverlayDisableParams]()
+ c.commandResultSchemas["Overlay.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayDisableResult]())
+ c.commandParamsSchemas["Overlay.enable"] = abxjsonschema.SchemaFor[OverlayEnableParams]()
+ c.commandResultSchemas["Overlay.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayEnableResult]())
+ c.commandParamsSchemas["Overlay.getHighlightObjectForTest"] = abxjsonschema.SchemaFor[OverlayGetHighlightObjectForTestParams]()
+ c.commandResultSchemas["Overlay.getHighlightObjectForTest"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayGetHighlightObjectForTestResult]())
+ c.commandParamsSchemas["Overlay.getGridHighlightObjectsForTest"] = abxjsonschema.SchemaFor[OverlayGetGridHighlightObjectsForTestParams]()
+ c.commandResultSchemas["Overlay.getGridHighlightObjectsForTest"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayGetGridHighlightObjectsForTestResult]())
+ c.commandParamsSchemas["Overlay.getSourceOrderHighlightObjectForTest"] = abxjsonschema.SchemaFor[OverlayGetSourceOrderHighlightObjectForTestParams]()
+ c.commandResultSchemas["Overlay.getSourceOrderHighlightObjectForTest"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayGetSourceOrderHighlightObjectForTestResult]())
+ c.commandParamsSchemas["Overlay.hideHighlight"] = abxjsonschema.SchemaFor[OverlayHideHighlightParams]()
+ c.commandResultSchemas["Overlay.hideHighlight"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHideHighlightResult]())
+ c.commandParamsSchemas["Overlay.highlightFrame"] = abxjsonschema.SchemaFor[OverlayHighlightFrameParams]()
+ c.commandResultSchemas["Overlay.highlightFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightFrameResult]())
+ c.commandParamsSchemas["Overlay.highlightNode"] = abxjsonschema.SchemaFor[OverlayHighlightNodeParams]()
+ c.commandResultSchemas["Overlay.highlightNode"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightNodeResult]())
+ c.commandParamsSchemas["Overlay.highlightQuad"] = abxjsonschema.SchemaFor[OverlayHighlightQuadParams]()
+ c.commandResultSchemas["Overlay.highlightQuad"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightQuadResult]())
+ c.commandParamsSchemas["Overlay.highlightRect"] = abxjsonschema.SchemaFor[OverlayHighlightRectParams]()
+ c.commandResultSchemas["Overlay.highlightRect"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightRectResult]())
+ c.commandParamsSchemas["Overlay.highlightSourceOrder"] = abxjsonschema.SchemaFor[OverlayHighlightSourceOrderParams]()
+ c.commandResultSchemas["Overlay.highlightSourceOrder"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightSourceOrderResult]())
+ c.commandParamsSchemas["Overlay.setInspectMode"] = abxjsonschema.SchemaFor[OverlaySetInspectModeParams]()
+ c.commandResultSchemas["Overlay.setInspectMode"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetInspectModeResult]())
+ c.commandParamsSchemas["Overlay.setShowAdHighlights"] = abxjsonschema.SchemaFor[OverlaySetShowAdHighlightsParams]()
+ c.commandResultSchemas["Overlay.setShowAdHighlights"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowAdHighlightsResult]())
+ c.commandParamsSchemas["Overlay.setPausedInDebuggerMessage"] = abxjsonschema.SchemaFor[OverlaySetPausedInDebuggerMessageParams]()
+ c.commandResultSchemas["Overlay.setPausedInDebuggerMessage"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetPausedInDebuggerMessageResult]())
+ c.commandParamsSchemas["Overlay.setShowDebugBorders"] = abxjsonschema.SchemaFor[OverlaySetShowDebugBordersParams]()
+ c.commandResultSchemas["Overlay.setShowDebugBorders"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowDebugBordersResult]())
+ c.commandParamsSchemas["Overlay.setShowFPSCounter"] = abxjsonschema.SchemaFor[OverlaySetShowFPSCounterParams]()
+ c.commandResultSchemas["Overlay.setShowFPSCounter"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowFPSCounterResult]())
+ c.commandParamsSchemas["Overlay.setShowGridOverlays"] = abxjsonschema.SchemaFor[OverlaySetShowGridOverlaysParams]()
+ c.commandResultSchemas["Overlay.setShowGridOverlays"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowGridOverlaysResult]())
+ c.commandParamsSchemas["Overlay.setShowFlexOverlays"] = abxjsonschema.SchemaFor[OverlaySetShowFlexOverlaysParams]()
+ c.commandResultSchemas["Overlay.setShowFlexOverlays"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowFlexOverlaysResult]())
+ c.commandParamsSchemas["Overlay.setShowScrollSnapOverlays"] = abxjsonschema.SchemaFor[OverlaySetShowScrollSnapOverlaysParams]()
+ c.commandResultSchemas["Overlay.setShowScrollSnapOverlays"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowScrollSnapOverlaysResult]())
+ c.commandParamsSchemas["Overlay.setShowContainerQueryOverlays"] = abxjsonschema.SchemaFor[OverlaySetShowContainerQueryOverlaysParams]()
+ c.commandResultSchemas["Overlay.setShowContainerQueryOverlays"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowContainerQueryOverlaysResult]())
+ c.commandParamsSchemas["Overlay.setShowInspectedElementAnchor"] = abxjsonschema.SchemaFor[OverlaySetShowInspectedElementAnchorParams]()
+ c.commandResultSchemas["Overlay.setShowInspectedElementAnchor"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowInspectedElementAnchorResult]())
+ c.commandParamsSchemas["Overlay.setShowPaintRects"] = abxjsonschema.SchemaFor[OverlaySetShowPaintRectsParams]()
+ c.commandResultSchemas["Overlay.setShowPaintRects"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowPaintRectsResult]())
+ c.commandParamsSchemas["Overlay.setShowLayoutShiftRegions"] = abxjsonschema.SchemaFor[OverlaySetShowLayoutShiftRegionsParams]()
+ c.commandResultSchemas["Overlay.setShowLayoutShiftRegions"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowLayoutShiftRegionsResult]())
+ c.commandParamsSchemas["Overlay.setShowScrollBottleneckRects"] = abxjsonschema.SchemaFor[OverlaySetShowScrollBottleneckRectsParams]()
+ c.commandResultSchemas["Overlay.setShowScrollBottleneckRects"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowScrollBottleneckRectsResult]())
+ c.commandParamsSchemas["Overlay.setShowHitTestBorders"] = abxjsonschema.SchemaFor[OverlaySetShowHitTestBordersParams]()
+ c.commandResultSchemas["Overlay.setShowHitTestBorders"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowHitTestBordersResult]())
+ c.commandParamsSchemas["Overlay.setShowWebVitals"] = abxjsonschema.SchemaFor[OverlaySetShowWebVitalsParams]()
+ c.commandResultSchemas["Overlay.setShowWebVitals"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowWebVitalsResult]())
+ c.commandParamsSchemas["Overlay.setShowViewportSizeOnResize"] = abxjsonschema.SchemaFor[OverlaySetShowViewportSizeOnResizeParams]()
+ c.commandResultSchemas["Overlay.setShowViewportSizeOnResize"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowViewportSizeOnResizeResult]())
+ c.commandParamsSchemas["Overlay.setShowHinge"] = abxjsonschema.SchemaFor[OverlaySetShowHingeParams]()
+ c.commandResultSchemas["Overlay.setShowHinge"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowHingeResult]())
+ c.commandParamsSchemas["Overlay.setShowIsolatedElements"] = abxjsonschema.SchemaFor[OverlaySetShowIsolatedElementsParams]()
+ c.commandResultSchemas["Overlay.setShowIsolatedElements"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowIsolatedElementsResult]())
+ c.commandParamsSchemas["Overlay.setShowWindowControlsOverlay"] = abxjsonschema.SchemaFor[OverlaySetShowWindowControlsOverlayParams]()
+ c.commandResultSchemas["Overlay.setShowWindowControlsOverlay"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowWindowControlsOverlayResult]())
+ c.commandParamsSchemas["PWA.getOsAppState"] = abxjsonschema.SchemaFor[PWAGetOsAppStateParams]()
+ c.commandResultSchemas["PWA.getOsAppState"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAGetOsAppStateResult]())
+ c.commandParamsSchemas["PWA.install"] = abxjsonschema.SchemaFor[PWAInstallParams]()
+ c.commandResultSchemas["PWA.install"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAInstallResult]())
+ c.commandParamsSchemas["PWA.uninstall"] = abxjsonschema.SchemaFor[PWAUninstallParams]()
+ c.commandResultSchemas["PWA.uninstall"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAUninstallResult]())
+ c.commandParamsSchemas["PWA.launch"] = abxjsonschema.SchemaFor[PWALaunchParams]()
+ c.commandResultSchemas["PWA.launch"] = nativeResultSchema(abxjsonschema.SchemaFor[PWALaunchResult]())
+ c.commandParamsSchemas["PWA.launchFilesInApp"] = abxjsonschema.SchemaFor[PWALaunchFilesInAppParams]()
+ c.commandResultSchemas["PWA.launchFilesInApp"] = nativeResultSchema(abxjsonschema.SchemaFor[PWALaunchFilesInAppResult]())
+ c.commandParamsSchemas["PWA.openCurrentPageInApp"] = abxjsonschema.SchemaFor[PWAOpenCurrentPageInAppParams]()
+ c.commandResultSchemas["PWA.openCurrentPageInApp"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAOpenCurrentPageInAppResult]())
+ c.commandParamsSchemas["PWA.changeAppUserSettings"] = abxjsonschema.SchemaFor[PWAChangeAppUserSettingsParams]()
+ c.commandResultSchemas["PWA.changeAppUserSettings"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAChangeAppUserSettingsResult]())
+ c.commandParamsSchemas["Page.addScriptToEvaluateOnLoad"] = abxjsonschema.SchemaFor[PageAddScriptToEvaluateOnLoadParams]()
+ c.commandResultSchemas["Page.addScriptToEvaluateOnLoad"] = nativeResultSchema(abxjsonschema.SchemaFor[PageAddScriptToEvaluateOnLoadResult]())
+ c.commandParamsSchemas["Page.addScriptToEvaluateOnNewDocument"] = abxjsonschema.SchemaFor[PageAddScriptToEvaluateOnNewDocumentParams]()
+ c.commandResultSchemas["Page.addScriptToEvaluateOnNewDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[PageAddScriptToEvaluateOnNewDocumentResult]())
+ c.commandParamsSchemas["Page.bringToFront"] = abxjsonschema.SchemaFor[PageBringToFrontParams]()
+ c.commandResultSchemas["Page.bringToFront"] = nativeResultSchema(abxjsonschema.SchemaFor[PageBringToFrontResult]())
+ c.commandParamsSchemas["Page.captureScreenshot"] = abxjsonschema.SchemaFor[PageCaptureScreenshotParams]()
+ c.commandResultSchemas["Page.captureScreenshot"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCaptureScreenshotResult]())
+ c.commandParamsSchemas["Page.captureSnapshot"] = abxjsonschema.SchemaFor[PageCaptureSnapshotParams]()
+ c.commandResultSchemas["Page.captureSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCaptureSnapshotResult]())
+ c.commandParamsSchemas["Page.clearDeviceMetricsOverride"] = abxjsonschema.SchemaFor[PageClearDeviceMetricsOverrideParams]()
+ c.commandResultSchemas["Page.clearDeviceMetricsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageClearDeviceMetricsOverrideResult]())
+ c.commandParamsSchemas["Page.clearDeviceOrientationOverride"] = abxjsonschema.SchemaFor[PageClearDeviceOrientationOverrideParams]()
+ c.commandResultSchemas["Page.clearDeviceOrientationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageClearDeviceOrientationOverrideResult]())
+ c.commandParamsSchemas["Page.clearGeolocationOverride"] = abxjsonschema.SchemaFor[PageClearGeolocationOverrideParams]()
+ c.commandResultSchemas["Page.clearGeolocationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageClearGeolocationOverrideResult]())
+ c.commandParamsSchemas["Page.createIsolatedWorld"] = abxjsonschema.SchemaFor[PageCreateIsolatedWorldParams]()
+ c.commandResultSchemas["Page.createIsolatedWorld"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCreateIsolatedWorldResult]())
+ c.commandParamsSchemas["Page.deleteCookie"] = abxjsonschema.SchemaFor[PageDeleteCookieParams]()
+ c.commandResultSchemas["Page.deleteCookie"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDeleteCookieResult]())
+ c.commandParamsSchemas["Page.disable"] = abxjsonschema.SchemaFor[PageDisableParams]()
+ c.commandResultSchemas["Page.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDisableResult]())
+ c.commandParamsSchemas["Page.enable"] = abxjsonschema.SchemaFor[PageEnableParams]()
+ c.commandResultSchemas["Page.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[PageEnableResult]())
+ c.commandParamsSchemas["Page.getAppManifest"] = abxjsonschema.SchemaFor[PageGetAppManifestParams]()
+ c.commandResultSchemas["Page.getAppManifest"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetAppManifestResult]())
+ c.commandParamsSchemas["Page.getInstallabilityErrors"] = abxjsonschema.SchemaFor[PageGetInstallabilityErrorsParams]()
+ c.commandResultSchemas["Page.getInstallabilityErrors"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetInstallabilityErrorsResult]())
+ c.commandParamsSchemas["Page.getManifestIcons"] = abxjsonschema.SchemaFor[PageGetManifestIconsParams]()
+ c.commandResultSchemas["Page.getManifestIcons"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetManifestIconsResult]())
+ c.commandParamsSchemas["Page.getAppId"] = abxjsonschema.SchemaFor[PageGetAppIDParams]()
+ c.commandResultSchemas["Page.getAppId"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetAppIDResult]())
+ c.commandParamsSchemas["Page.getAdScriptAncestry"] = abxjsonschema.SchemaFor[PageGetAdScriptAncestryParams]()
+ c.commandResultSchemas["Page.getAdScriptAncestry"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetAdScriptAncestryResult]())
+ c.commandParamsSchemas["Page.getFrameTree"] = abxjsonschema.SchemaFor[PageGetFrameTreeParams]()
+ c.commandResultSchemas["Page.getFrameTree"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetFrameTreeResult]())
+ c.commandParamsSchemas["Page.getLayoutMetrics"] = abxjsonschema.SchemaFor[PageGetLayoutMetricsParams]()
+ c.commandResultSchemas["Page.getLayoutMetrics"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetLayoutMetricsResult]())
+ c.commandParamsSchemas["Page.getNavigationHistory"] = abxjsonschema.SchemaFor[PageGetNavigationHistoryParams]()
+ c.commandResultSchemas["Page.getNavigationHistory"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetNavigationHistoryResult]())
+ c.commandParamsSchemas["Page.resetNavigationHistory"] = abxjsonschema.SchemaFor[PageResetNavigationHistoryParams]()
+ c.commandResultSchemas["Page.resetNavigationHistory"] = nativeResultSchema(abxjsonschema.SchemaFor[PageResetNavigationHistoryResult]())
+ c.commandParamsSchemas["Page.getResourceContent"] = abxjsonschema.SchemaFor[PageGetResourceContentParams]()
+ c.commandResultSchemas["Page.getResourceContent"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetResourceContentResult]())
+ c.commandParamsSchemas["Page.getResourceTree"] = abxjsonschema.SchemaFor[PageGetResourceTreeParams]()
+ c.commandResultSchemas["Page.getResourceTree"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetResourceTreeResult]())
+ c.commandParamsSchemas["Page.handleJavaScriptDialog"] = abxjsonschema.SchemaFor[PageHandleJavaScriptDialogParams]()
+ c.commandResultSchemas["Page.handleJavaScriptDialog"] = nativeResultSchema(abxjsonschema.SchemaFor[PageHandleJavaScriptDialogResult]())
+ c.commandParamsSchemas["Page.navigate"] = abxjsonschema.SchemaFor[PageNavigateParams]()
+ c.commandResultSchemas["Page.navigate"] = nativeResultSchema(abxjsonschema.SchemaFor[PageNavigateResult]())
+ c.commandParamsSchemas["Page.navigateToHistoryEntry"] = abxjsonschema.SchemaFor[PageNavigateToHistoryEntryParams]()
+ c.commandResultSchemas["Page.navigateToHistoryEntry"] = nativeResultSchema(abxjsonschema.SchemaFor[PageNavigateToHistoryEntryResult]())
+ c.commandParamsSchemas["Page.printToPDF"] = abxjsonschema.SchemaFor[PagePrintToPDFParams]()
+ c.commandResultSchemas["Page.printToPDF"] = nativeResultSchema(abxjsonschema.SchemaFor[PagePrintToPDFResult]())
+ c.commandParamsSchemas["Page.reload"] = abxjsonschema.SchemaFor[PageReloadParams]()
+ c.commandResultSchemas["Page.reload"] = nativeResultSchema(abxjsonschema.SchemaFor[PageReloadResult]())
+ c.commandParamsSchemas["Page.removeScriptToEvaluateOnLoad"] = abxjsonschema.SchemaFor[PageRemoveScriptToEvaluateOnLoadParams]()
+ c.commandResultSchemas["Page.removeScriptToEvaluateOnLoad"] = nativeResultSchema(abxjsonschema.SchemaFor[PageRemoveScriptToEvaluateOnLoadResult]())
+ c.commandParamsSchemas["Page.removeScriptToEvaluateOnNewDocument"] = abxjsonschema.SchemaFor[PageRemoveScriptToEvaluateOnNewDocumentParams]()
+ c.commandResultSchemas["Page.removeScriptToEvaluateOnNewDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[PageRemoveScriptToEvaluateOnNewDocumentResult]())
+ c.commandParamsSchemas["Page.screencastFrameAck"] = abxjsonschema.SchemaFor[PageScreencastFrameAckParams]()
+ c.commandResultSchemas["Page.screencastFrameAck"] = nativeResultSchema(abxjsonschema.SchemaFor[PageScreencastFrameAckResult]())
+ c.commandParamsSchemas["Page.searchInResource"] = abxjsonschema.SchemaFor[PageSearchInResourceParams]()
+ c.commandResultSchemas["Page.searchInResource"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSearchInResourceResult]())
+ c.commandParamsSchemas["Page.setAdBlockingEnabled"] = abxjsonschema.SchemaFor[PageSetAdBlockingEnabledParams]()
+ c.commandResultSchemas["Page.setAdBlockingEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetAdBlockingEnabledResult]())
+ c.commandParamsSchemas["Page.setBypassCSP"] = abxjsonschema.SchemaFor[PageSetBypassCSPParams]()
+ c.commandResultSchemas["Page.setBypassCSP"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetBypassCSPResult]())
+ c.commandParamsSchemas["Page.getPermissionsPolicyState"] = abxjsonschema.SchemaFor[PageGetPermissionsPolicyStateParams]()
+ c.commandResultSchemas["Page.getPermissionsPolicyState"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetPermissionsPolicyStateResult]())
+ c.commandParamsSchemas["Page.getOriginTrials"] = abxjsonschema.SchemaFor[PageGetOriginTrialsParams]()
+ c.commandResultSchemas["Page.getOriginTrials"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetOriginTrialsResult]())
+ c.commandParamsSchemas["Page.setDeviceMetricsOverride"] = abxjsonschema.SchemaFor[PageSetDeviceMetricsOverrideParams]()
+ c.commandResultSchemas["Page.setDeviceMetricsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetDeviceMetricsOverrideResult]())
+ c.commandParamsSchemas["Page.setDeviceOrientationOverride"] = abxjsonschema.SchemaFor[PageSetDeviceOrientationOverrideParams]()
+ c.commandResultSchemas["Page.setDeviceOrientationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetDeviceOrientationOverrideResult]())
+ c.commandParamsSchemas["Page.setFontFamilies"] = abxjsonschema.SchemaFor[PageSetFontFamiliesParams]()
+ c.commandResultSchemas["Page.setFontFamilies"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetFontFamiliesResult]())
+ c.commandParamsSchemas["Page.setFontSizes"] = abxjsonschema.SchemaFor[PageSetFontSizesParams]()
+ c.commandResultSchemas["Page.setFontSizes"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetFontSizesResult]())
+ c.commandParamsSchemas["Page.setDocumentContent"] = abxjsonschema.SchemaFor[PageSetDocumentContentParams]()
+ c.commandResultSchemas["Page.setDocumentContent"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetDocumentContentResult]())
+ c.commandParamsSchemas["Page.setDownloadBehavior"] = abxjsonschema.SchemaFor[PageSetDownloadBehaviorParams]()
+ c.commandResultSchemas["Page.setDownloadBehavior"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetDownloadBehaviorResult]())
+ c.commandParamsSchemas["Page.setGeolocationOverride"] = abxjsonschema.SchemaFor[PageSetGeolocationOverrideParams]()
+ c.commandResultSchemas["Page.setGeolocationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetGeolocationOverrideResult]())
+ c.commandParamsSchemas["Page.setLifecycleEventsEnabled"] = abxjsonschema.SchemaFor[PageSetLifecycleEventsEnabledParams]()
+ c.commandResultSchemas["Page.setLifecycleEventsEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetLifecycleEventsEnabledResult]())
+ c.commandParamsSchemas["Page.setTouchEmulationEnabled"] = abxjsonschema.SchemaFor[PageSetTouchEmulationEnabledParams]()
+ c.commandResultSchemas["Page.setTouchEmulationEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetTouchEmulationEnabledResult]())
+ c.commandParamsSchemas["Page.startScreencast"] = abxjsonschema.SchemaFor[PageStartScreencastParams]()
+ c.commandResultSchemas["Page.startScreencast"] = nativeResultSchema(abxjsonschema.SchemaFor[PageStartScreencastResult]())
+ c.commandParamsSchemas["Page.stopLoading"] = abxjsonschema.SchemaFor[PageStopLoadingParams]()
+ c.commandResultSchemas["Page.stopLoading"] = nativeResultSchema(abxjsonschema.SchemaFor[PageStopLoadingResult]())
+ c.commandParamsSchemas["Page.crash"] = abxjsonschema.SchemaFor[PageCrashParams]()
+ c.commandResultSchemas["Page.crash"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCrashResult]())
+ c.commandParamsSchemas["Page.close"] = abxjsonschema.SchemaFor[PageCloseParams]()
+ c.commandResultSchemas["Page.close"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCloseResult]())
+ c.commandParamsSchemas["Page.setWebLifecycleState"] = abxjsonschema.SchemaFor[PageSetWebLifecycleStateParams]()
+ c.commandResultSchemas["Page.setWebLifecycleState"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetWebLifecycleStateResult]())
+ c.commandParamsSchemas["Page.stopScreencast"] = abxjsonschema.SchemaFor[PageStopScreencastParams]()
+ c.commandResultSchemas["Page.stopScreencast"] = nativeResultSchema(abxjsonschema.SchemaFor[PageStopScreencastResult]())
+ c.commandParamsSchemas["Page.produceCompilationCache"] = abxjsonschema.SchemaFor[PageProduceCompilationCacheParams]()
+ c.commandResultSchemas["Page.produceCompilationCache"] = nativeResultSchema(abxjsonschema.SchemaFor[PageProduceCompilationCacheResult]())
+ c.commandParamsSchemas["Page.addCompilationCache"] = abxjsonschema.SchemaFor[PageAddCompilationCacheParams]()
+ c.commandResultSchemas["Page.addCompilationCache"] = nativeResultSchema(abxjsonschema.SchemaFor[PageAddCompilationCacheResult]())
+ c.commandParamsSchemas["Page.clearCompilationCache"] = abxjsonschema.SchemaFor[PageClearCompilationCacheParams]()
+ c.commandResultSchemas["Page.clearCompilationCache"] = nativeResultSchema(abxjsonschema.SchemaFor[PageClearCompilationCacheResult]())
+ c.commandParamsSchemas["Page.setSPCTransactionMode"] = abxjsonschema.SchemaFor[PageSetSPCTransactionModeParams]()
+ c.commandResultSchemas["Page.setSPCTransactionMode"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetSPCTransactionModeResult]())
+ c.commandParamsSchemas["Page.setRPHRegistrationMode"] = abxjsonschema.SchemaFor[PageSetRPHRegistrationModeParams]()
+ c.commandResultSchemas["Page.setRPHRegistrationMode"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetRPHRegistrationModeResult]())
+ c.commandParamsSchemas["Page.generateTestReport"] = abxjsonschema.SchemaFor[PageGenerateTestReportParams]()
+ c.commandResultSchemas["Page.generateTestReport"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGenerateTestReportResult]())
+ c.commandParamsSchemas["Page.waitForDebugger"] = abxjsonschema.SchemaFor[PageWaitForDebuggerParams]()
+ c.commandResultSchemas["Page.waitForDebugger"] = nativeResultSchema(abxjsonschema.SchemaFor[PageWaitForDebuggerResult]())
+ c.commandParamsSchemas["Page.setInterceptFileChooserDialog"] = abxjsonschema.SchemaFor[PageSetInterceptFileChooserDialogParams]()
+ c.commandResultSchemas["Page.setInterceptFileChooserDialog"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetInterceptFileChooserDialogResult]())
+ c.commandParamsSchemas["Page.setPrerenderingAllowed"] = abxjsonschema.SchemaFor[PageSetPrerenderingAllowedParams]()
+ c.commandResultSchemas["Page.setPrerenderingAllowed"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetPrerenderingAllowedResult]())
+ c.commandParamsSchemas["Page.getAnnotatedPageContent"] = abxjsonschema.SchemaFor[PageGetAnnotatedPageContentParams]()
+ c.commandResultSchemas["Page.getAnnotatedPageContent"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetAnnotatedPageContentResult]())
+ c.commandParamsSchemas["Performance.disable"] = abxjsonschema.SchemaFor[PerformanceDisableParams]()
+ c.commandResultSchemas["Performance.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceDisableResult]())
+ c.commandParamsSchemas["Performance.enable"] = abxjsonschema.SchemaFor[PerformanceEnableParams]()
+ c.commandResultSchemas["Performance.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceEnableResult]())
+ c.commandParamsSchemas["Performance.setTimeDomain"] = abxjsonschema.SchemaFor[PerformanceSetTimeDomainParams]()
+ c.commandResultSchemas["Performance.setTimeDomain"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceSetTimeDomainResult]())
+ c.commandParamsSchemas["Performance.getMetrics"] = abxjsonschema.SchemaFor[PerformanceGetMetricsParams]()
+ c.commandResultSchemas["Performance.getMetrics"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceGetMetricsResult]())
+ c.commandParamsSchemas["PerformanceTimeline.enable"] = abxjsonschema.SchemaFor[PerformanceTimelineEnableParams]()
+ c.commandResultSchemas["PerformanceTimeline.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceTimelineEnableResult]())
+ c.commandParamsSchemas["Preload.enable"] = abxjsonschema.SchemaFor[PreloadEnableParams]()
+ c.commandResultSchemas["Preload.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadEnableResult]())
+ c.commandParamsSchemas["Preload.disable"] = abxjsonschema.SchemaFor[PreloadDisableParams]()
+ c.commandResultSchemas["Preload.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadDisableResult]())
+ c.commandParamsSchemas["Profiler.disable"] = abxjsonschema.SchemaFor[ProfilerDisableParams]()
+ c.commandResultSchemas["Profiler.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerDisableResult]())
+ c.commandParamsSchemas["Profiler.enable"] = abxjsonschema.SchemaFor[ProfilerEnableParams]()
+ c.commandResultSchemas["Profiler.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerEnableResult]())
+ c.commandParamsSchemas["Profiler.getBestEffortCoverage"] = abxjsonschema.SchemaFor[ProfilerGetBestEffortCoverageParams]()
+ c.commandResultSchemas["Profiler.getBestEffortCoverage"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerGetBestEffortCoverageResult]())
+ c.commandParamsSchemas["Profiler.setSamplingInterval"] = abxjsonschema.SchemaFor[ProfilerSetSamplingIntervalParams]()
+ c.commandResultSchemas["Profiler.setSamplingInterval"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerSetSamplingIntervalResult]())
+ c.commandParamsSchemas["Profiler.start"] = abxjsonschema.SchemaFor[ProfilerStartParams]()
+ c.commandResultSchemas["Profiler.start"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerStartResult]())
+ c.commandParamsSchemas["Profiler.startPreciseCoverage"] = abxjsonschema.SchemaFor[ProfilerStartPreciseCoverageParams]()
+ c.commandResultSchemas["Profiler.startPreciseCoverage"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerStartPreciseCoverageResult]())
+ c.commandParamsSchemas["Profiler.stop"] = abxjsonschema.SchemaFor[ProfilerStopParams]()
+ c.commandResultSchemas["Profiler.stop"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerStopResult]())
+ c.commandParamsSchemas["Profiler.stopPreciseCoverage"] = abxjsonschema.SchemaFor[ProfilerStopPreciseCoverageParams]()
+ c.commandResultSchemas["Profiler.stopPreciseCoverage"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerStopPreciseCoverageResult]())
+ c.commandParamsSchemas["Profiler.takePreciseCoverage"] = abxjsonschema.SchemaFor[ProfilerTakePreciseCoverageParams]()
+ c.commandResultSchemas["Profiler.takePreciseCoverage"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerTakePreciseCoverageResult]())
+ c.commandParamsSchemas["Runtime.awaitPromise"] = abxjsonschema.SchemaFor[RuntimeAwaitPromiseParams]()
+ c.commandResultSchemas["Runtime.awaitPromise"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeAwaitPromiseResult]())
+ c.commandParamsSchemas["Runtime.callFunctionOn"] = abxjsonschema.SchemaFor[RuntimeCallFunctionOnParams]()
+ c.commandResultSchemas["Runtime.callFunctionOn"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeCallFunctionOnResult]())
+ c.commandParamsSchemas["Runtime.compileScript"] = abxjsonschema.SchemaFor[RuntimeCompileScriptParams]()
+ c.commandResultSchemas["Runtime.compileScript"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeCompileScriptResult]())
+ c.commandParamsSchemas["Runtime.disable"] = abxjsonschema.SchemaFor[RuntimeDisableParams]()
+ c.commandResultSchemas["Runtime.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeDisableResult]())
+ c.commandParamsSchemas["Runtime.discardConsoleEntries"] = abxjsonschema.SchemaFor[RuntimeDiscardConsoleEntriesParams]()
+ c.commandResultSchemas["Runtime.discardConsoleEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeDiscardConsoleEntriesResult]())
+ c.commandParamsSchemas["Runtime.enable"] = abxjsonschema.SchemaFor[RuntimeEnableParams]()
+ c.commandResultSchemas["Runtime.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeEnableResult]())
+ c.commandParamsSchemas["Runtime.evaluate"] = abxjsonschema.SchemaFor[RuntimeEvaluateParams]()
+ c.commandResultSchemas["Runtime.evaluate"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeEvaluateResult]())
+ c.commandParamsSchemas["Runtime.getIsolateId"] = abxjsonschema.SchemaFor[RuntimeGetIsolateIDParams]()
+ c.commandResultSchemas["Runtime.getIsolateId"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGetIsolateIDResult]())
+ c.commandParamsSchemas["Runtime.getHeapUsage"] = abxjsonschema.SchemaFor[RuntimeGetHeapUsageParams]()
+ c.commandResultSchemas["Runtime.getHeapUsage"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGetHeapUsageResult]())
+ c.commandParamsSchemas["Runtime.getProperties"] = abxjsonschema.SchemaFor[RuntimeGetPropertiesParams]()
+ c.commandResultSchemas["Runtime.getProperties"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGetPropertiesResult]())
+ c.commandParamsSchemas["Runtime.globalLexicalScopeNames"] = abxjsonschema.SchemaFor[RuntimeGlobalLexicalScopeNamesParams]()
+ c.commandResultSchemas["Runtime.globalLexicalScopeNames"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGlobalLexicalScopeNamesResult]())
+ c.commandParamsSchemas["Runtime.queryObjects"] = abxjsonschema.SchemaFor[RuntimeQueryObjectsParams]()
+ c.commandResultSchemas["Runtime.queryObjects"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeQueryObjectsResult]())
+ c.commandParamsSchemas["Runtime.releaseObject"] = abxjsonschema.SchemaFor[RuntimeReleaseObjectParams]()
+ c.commandResultSchemas["Runtime.releaseObject"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeReleaseObjectResult]())
+ c.commandParamsSchemas["Runtime.releaseObjectGroup"] = abxjsonschema.SchemaFor[RuntimeReleaseObjectGroupParams]()
+ c.commandResultSchemas["Runtime.releaseObjectGroup"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeReleaseObjectGroupResult]())
+ c.commandParamsSchemas["Runtime.runIfWaitingForDebugger"] = abxjsonschema.SchemaFor[RuntimeRunIfWaitingForDebuggerParams]()
+ c.commandResultSchemas["Runtime.runIfWaitingForDebugger"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeRunIfWaitingForDebuggerResult]())
+ c.commandParamsSchemas["Runtime.runScript"] = abxjsonschema.SchemaFor[RuntimeRunScriptParams]()
+ c.commandResultSchemas["Runtime.runScript"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeRunScriptResult]())
+ c.commandParamsSchemas["Runtime.setAsyncCallStackDepth"] = abxjsonschema.SchemaFor[RuntimeSetAsyncCallStackDepthParams]()
+ c.commandResultSchemas["Runtime.setAsyncCallStackDepth"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeSetAsyncCallStackDepthResult]())
+ c.commandParamsSchemas["Runtime.setCustomObjectFormatterEnabled"] = abxjsonschema.SchemaFor[RuntimeSetCustomObjectFormatterEnabledParams]()
+ c.commandResultSchemas["Runtime.setCustomObjectFormatterEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeSetCustomObjectFormatterEnabledResult]())
+ c.commandParamsSchemas["Runtime.setMaxCallStackSizeToCapture"] = abxjsonschema.SchemaFor[RuntimeSetMaxCallStackSizeToCaptureParams]()
+ c.commandResultSchemas["Runtime.setMaxCallStackSizeToCapture"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeSetMaxCallStackSizeToCaptureResult]())
+ c.commandParamsSchemas["Runtime.terminateExecution"] = abxjsonschema.SchemaFor[RuntimeTerminateExecutionParams]()
+ c.commandResultSchemas["Runtime.terminateExecution"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeTerminateExecutionResult]())
+ c.commandParamsSchemas["Runtime.addBinding"] = abxjsonschema.SchemaFor[RuntimeAddBindingParams]()
+ c.commandResultSchemas["Runtime.addBinding"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeAddBindingResult]())
+ c.commandParamsSchemas["Runtime.removeBinding"] = abxjsonschema.SchemaFor[RuntimeRemoveBindingParams]()
+ c.commandResultSchemas["Runtime.removeBinding"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeRemoveBindingResult]())
+ c.commandParamsSchemas["Runtime.getExceptionDetails"] = abxjsonschema.SchemaFor[RuntimeGetExceptionDetailsParams]()
+ c.commandResultSchemas["Runtime.getExceptionDetails"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGetExceptionDetailsResult]())
+ c.commandParamsSchemas["Schema.getDomains"] = abxjsonschema.SchemaFor[SchemaGetDomainsParams]()
+ c.commandResultSchemas["Schema.getDomains"] = nativeResultSchema(abxjsonschema.SchemaFor[SchemaGetDomainsResult]())
+ c.commandParamsSchemas["Security.disable"] = abxjsonschema.SchemaFor[SecurityDisableParams]()
+ c.commandResultSchemas["Security.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityDisableResult]())
+ c.commandParamsSchemas["Security.enable"] = abxjsonschema.SchemaFor[SecurityEnableParams]()
+ c.commandResultSchemas["Security.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityEnableResult]())
+ c.commandParamsSchemas["Security.setIgnoreCertificateErrors"] = abxjsonschema.SchemaFor[SecuritySetIgnoreCertificateErrorsParams]()
+ c.commandResultSchemas["Security.setIgnoreCertificateErrors"] = nativeResultSchema(abxjsonschema.SchemaFor[SecuritySetIgnoreCertificateErrorsResult]())
+ c.commandParamsSchemas["Security.handleCertificateError"] = abxjsonschema.SchemaFor[SecurityHandleCertificateErrorParams]()
+ c.commandResultSchemas["Security.handleCertificateError"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityHandleCertificateErrorResult]())
+ c.commandParamsSchemas["Security.setOverrideCertificateErrors"] = abxjsonschema.SchemaFor[SecuritySetOverrideCertificateErrorsParams]()
+ c.commandResultSchemas["Security.setOverrideCertificateErrors"] = nativeResultSchema(abxjsonschema.SchemaFor[SecuritySetOverrideCertificateErrorsResult]())
+ c.commandParamsSchemas["ServiceWorker.deliverPushMessage"] = abxjsonschema.SchemaFor[ServiceWorkerDeliverPushMessageParams]()
+ c.commandResultSchemas["ServiceWorker.deliverPushMessage"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerDeliverPushMessageResult]())
+ c.commandParamsSchemas["ServiceWorker.disable"] = abxjsonschema.SchemaFor[ServiceWorkerDisableParams]()
+ c.commandResultSchemas["ServiceWorker.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerDisableResult]())
+ c.commandParamsSchemas["ServiceWorker.dispatchSyncEvent"] = abxjsonschema.SchemaFor[ServiceWorkerDispatchSyncEventParams]()
+ c.commandResultSchemas["ServiceWorker.dispatchSyncEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerDispatchSyncEventResult]())
+ c.commandParamsSchemas["ServiceWorker.dispatchPeriodicSyncEvent"] = abxjsonschema.SchemaFor[ServiceWorkerDispatchPeriodicSyncEventParams]()
+ c.commandResultSchemas["ServiceWorker.dispatchPeriodicSyncEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerDispatchPeriodicSyncEventResult]())
+ c.commandParamsSchemas["ServiceWorker.enable"] = abxjsonschema.SchemaFor[ServiceWorkerEnableParams]()
+ c.commandResultSchemas["ServiceWorker.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerEnableResult]())
+ c.commandParamsSchemas["ServiceWorker.setForceUpdateOnPageLoad"] = abxjsonschema.SchemaFor[ServiceWorkerSetForceUpdateOnPageLoadParams]()
+ c.commandResultSchemas["ServiceWorker.setForceUpdateOnPageLoad"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerSetForceUpdateOnPageLoadResult]())
+ c.commandParamsSchemas["ServiceWorker.skipWaiting"] = abxjsonschema.SchemaFor[ServiceWorkerSkipWaitingParams]()
+ c.commandResultSchemas["ServiceWorker.skipWaiting"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerSkipWaitingResult]())
+ c.commandParamsSchemas["ServiceWorker.startWorker"] = abxjsonschema.SchemaFor[ServiceWorkerStartWorkerParams]()
+ c.commandResultSchemas["ServiceWorker.startWorker"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerStartWorkerResult]())
+ c.commandParamsSchemas["ServiceWorker.stopAllWorkers"] = abxjsonschema.SchemaFor[ServiceWorkerStopAllWorkersParams]()
+ c.commandResultSchemas["ServiceWorker.stopAllWorkers"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerStopAllWorkersResult]())
+ c.commandParamsSchemas["ServiceWorker.stopWorker"] = abxjsonschema.SchemaFor[ServiceWorkerStopWorkerParams]()
+ c.commandResultSchemas["ServiceWorker.stopWorker"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerStopWorkerResult]())
+ c.commandParamsSchemas["ServiceWorker.unregister"] = abxjsonschema.SchemaFor[ServiceWorkerUnregisterParams]()
+ c.commandResultSchemas["ServiceWorker.unregister"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerUnregisterResult]())
+ c.commandParamsSchemas["ServiceWorker.updateRegistration"] = abxjsonschema.SchemaFor[ServiceWorkerUpdateRegistrationParams]()
+ c.commandResultSchemas["ServiceWorker.updateRegistration"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerUpdateRegistrationResult]())
+ c.commandParamsSchemas["SmartCardEmulation.enable"] = abxjsonschema.SchemaFor[SmartCardEmulationEnableParams]()
+ c.commandResultSchemas["SmartCardEmulation.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationEnableResult]())
+ c.commandParamsSchemas["SmartCardEmulation.disable"] = abxjsonschema.SchemaFor[SmartCardEmulationDisableParams]()
+ c.commandResultSchemas["SmartCardEmulation.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationDisableResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportEstablishContextResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportEstablishContextResultParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportEstablishContextResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportEstablishContextResultResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportReleaseContextResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportReleaseContextResultParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportReleaseContextResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportReleaseContextResultResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportListReadersResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportListReadersResultParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportListReadersResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportListReadersResultResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportGetStatusChangeResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportGetStatusChangeResultParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportGetStatusChangeResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportGetStatusChangeResultResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportBeginTransactionResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportBeginTransactionResultParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportBeginTransactionResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportBeginTransactionResultResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportPlainResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportPlainResultParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportPlainResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportPlainResultResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportConnectResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportConnectResultParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportConnectResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportConnectResultResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportDataResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportDataResultParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportDataResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportDataResultResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportStatusResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportStatusResultParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportStatusResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportStatusResultResult]())
+ c.commandParamsSchemas["SmartCardEmulation.reportError"] = abxjsonschema.SchemaFor[SmartCardEmulationReportErrorParams]()
+ c.commandResultSchemas["SmartCardEmulation.reportError"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportErrorResult]())
+ c.commandParamsSchemas["Storage.getStorageKeyForFrame"] = abxjsonschema.SchemaFor[StorageGetStorageKeyForFrameParams]()
+ c.commandResultSchemas["Storage.getStorageKeyForFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetStorageKeyForFrameResult]())
+ c.commandParamsSchemas["Storage.getStorageKey"] = abxjsonschema.SchemaFor[StorageGetStorageKeyParams]()
+ c.commandResultSchemas["Storage.getStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetStorageKeyResult]())
+ c.commandParamsSchemas["Storage.clearDataForOrigin"] = abxjsonschema.SchemaFor[StorageClearDataForOriginParams]()
+ c.commandResultSchemas["Storage.clearDataForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearDataForOriginResult]())
+ c.commandParamsSchemas["Storage.clearDataForStorageKey"] = abxjsonschema.SchemaFor[StorageClearDataForStorageKeyParams]()
+ c.commandResultSchemas["Storage.clearDataForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearDataForStorageKeyResult]())
+ c.commandParamsSchemas["Storage.getCookies"] = abxjsonschema.SchemaFor[StorageGetCookiesParams]()
+ c.commandResultSchemas["Storage.getCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetCookiesResult]())
+ c.commandParamsSchemas["Storage.setCookies"] = abxjsonschema.SchemaFor[StorageSetCookiesParams]()
+ c.commandResultSchemas["Storage.setCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetCookiesResult]())
+ c.commandParamsSchemas["Storage.clearCookies"] = abxjsonschema.SchemaFor[StorageClearCookiesParams]()
+ c.commandResultSchemas["Storage.clearCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearCookiesResult]())
+ c.commandParamsSchemas["Storage.getUsageAndQuota"] = abxjsonschema.SchemaFor[StorageGetUsageAndQuotaParams]()
+ c.commandResultSchemas["Storage.getUsageAndQuota"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetUsageAndQuotaResult]())
+ c.commandParamsSchemas["Storage.overrideQuotaForOrigin"] = abxjsonschema.SchemaFor[StorageOverrideQuotaForOriginParams]()
+ c.commandResultSchemas["Storage.overrideQuotaForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageOverrideQuotaForOriginResult]())
+ c.commandParamsSchemas["Storage.trackCacheStorageForOrigin"] = abxjsonschema.SchemaFor[StorageTrackCacheStorageForOriginParams]()
+ c.commandResultSchemas["Storage.trackCacheStorageForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageTrackCacheStorageForOriginResult]())
+ c.commandParamsSchemas["Storage.trackCacheStorageForStorageKey"] = abxjsonschema.SchemaFor[StorageTrackCacheStorageForStorageKeyParams]()
+ c.commandResultSchemas["Storage.trackCacheStorageForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageTrackCacheStorageForStorageKeyResult]())
+ c.commandParamsSchemas["Storage.trackIndexedDBForOrigin"] = abxjsonschema.SchemaFor[StorageTrackIndexedDBForOriginParams]()
+ c.commandResultSchemas["Storage.trackIndexedDBForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageTrackIndexedDBForOriginResult]())
+ c.commandParamsSchemas["Storage.trackIndexedDBForStorageKey"] = abxjsonschema.SchemaFor[StorageTrackIndexedDBForStorageKeyParams]()
+ c.commandResultSchemas["Storage.trackIndexedDBForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageTrackIndexedDBForStorageKeyResult]())
+ c.commandParamsSchemas["Storage.untrackCacheStorageForOrigin"] = abxjsonschema.SchemaFor[StorageUntrackCacheStorageForOriginParams]()
+ c.commandResultSchemas["Storage.untrackCacheStorageForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageUntrackCacheStorageForOriginResult]())
+ c.commandParamsSchemas["Storage.untrackCacheStorageForStorageKey"] = abxjsonschema.SchemaFor[StorageUntrackCacheStorageForStorageKeyParams]()
+ c.commandResultSchemas["Storage.untrackCacheStorageForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageUntrackCacheStorageForStorageKeyResult]())
+ c.commandParamsSchemas["Storage.untrackIndexedDBForOrigin"] = abxjsonschema.SchemaFor[StorageUntrackIndexedDBForOriginParams]()
+ c.commandResultSchemas["Storage.untrackIndexedDBForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageUntrackIndexedDBForOriginResult]())
+ c.commandParamsSchemas["Storage.untrackIndexedDBForStorageKey"] = abxjsonschema.SchemaFor[StorageUntrackIndexedDBForStorageKeyParams]()
+ c.commandResultSchemas["Storage.untrackIndexedDBForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageUntrackIndexedDBForStorageKeyResult]())
+ c.commandParamsSchemas["Storage.getTrustTokens"] = abxjsonschema.SchemaFor[StorageGetTrustTokensParams]()
+ c.commandResultSchemas["Storage.getTrustTokens"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetTrustTokensResult]())
+ c.commandParamsSchemas["Storage.clearTrustTokens"] = abxjsonschema.SchemaFor[StorageClearTrustTokensParams]()
+ c.commandResultSchemas["Storage.clearTrustTokens"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearTrustTokensResult]())
+ c.commandParamsSchemas["Storage.getInterestGroupDetails"] = abxjsonschema.SchemaFor[StorageGetInterestGroupDetailsParams]()
+ c.commandResultSchemas["Storage.getInterestGroupDetails"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetInterestGroupDetailsResult]())
+ c.commandParamsSchemas["Storage.setInterestGroupTracking"] = abxjsonschema.SchemaFor[StorageSetInterestGroupTrackingParams]()
+ c.commandResultSchemas["Storage.setInterestGroupTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetInterestGroupTrackingResult]())
+ c.commandParamsSchemas["Storage.setInterestGroupAuctionTracking"] = abxjsonschema.SchemaFor[StorageSetInterestGroupAuctionTrackingParams]()
+ c.commandResultSchemas["Storage.setInterestGroupAuctionTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetInterestGroupAuctionTrackingResult]())
+ c.commandParamsSchemas["Storage.getSharedStorageMetadata"] = abxjsonschema.SchemaFor[StorageGetSharedStorageMetadataParams]()
+ c.commandResultSchemas["Storage.getSharedStorageMetadata"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetSharedStorageMetadataResult]())
+ c.commandParamsSchemas["Storage.getSharedStorageEntries"] = abxjsonschema.SchemaFor[StorageGetSharedStorageEntriesParams]()
+ c.commandResultSchemas["Storage.getSharedStorageEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetSharedStorageEntriesResult]())
+ c.commandParamsSchemas["Storage.setSharedStorageEntry"] = abxjsonschema.SchemaFor[StorageSetSharedStorageEntryParams]()
+ c.commandResultSchemas["Storage.setSharedStorageEntry"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetSharedStorageEntryResult]())
+ c.commandParamsSchemas["Storage.deleteSharedStorageEntry"] = abxjsonschema.SchemaFor[StorageDeleteSharedStorageEntryParams]()
+ c.commandResultSchemas["Storage.deleteSharedStorageEntry"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageDeleteSharedStorageEntryResult]())
+ c.commandParamsSchemas["Storage.clearSharedStorageEntries"] = abxjsonschema.SchemaFor[StorageClearSharedStorageEntriesParams]()
+ c.commandResultSchemas["Storage.clearSharedStorageEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearSharedStorageEntriesResult]())
+ c.commandParamsSchemas["Storage.resetSharedStorageBudget"] = abxjsonschema.SchemaFor[StorageResetSharedStorageBudgetParams]()
+ c.commandResultSchemas["Storage.resetSharedStorageBudget"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageResetSharedStorageBudgetResult]())
+ c.commandParamsSchemas["Storage.setSharedStorageTracking"] = abxjsonschema.SchemaFor[StorageSetSharedStorageTrackingParams]()
+ c.commandResultSchemas["Storage.setSharedStorageTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetSharedStorageTrackingResult]())
+ c.commandParamsSchemas["Storage.setStorageBucketTracking"] = abxjsonschema.SchemaFor[StorageSetStorageBucketTrackingParams]()
+ c.commandResultSchemas["Storage.setStorageBucketTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetStorageBucketTrackingResult]())
+ c.commandParamsSchemas["Storage.deleteStorageBucket"] = abxjsonschema.SchemaFor[StorageDeleteStorageBucketParams]()
+ c.commandResultSchemas["Storage.deleteStorageBucket"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageDeleteStorageBucketResult]())
+ c.commandParamsSchemas["Storage.runBounceTrackingMitigations"] = abxjsonschema.SchemaFor[StorageRunBounceTrackingMitigationsParams]()
+ c.commandResultSchemas["Storage.runBounceTrackingMitigations"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageRunBounceTrackingMitigationsResult]())
+ c.commandParamsSchemas["Storage.setAttributionReportingLocalTestingMode"] = abxjsonschema.SchemaFor[StorageSetAttributionReportingLocalTestingModeParams]()
+ c.commandResultSchemas["Storage.setAttributionReportingLocalTestingMode"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetAttributionReportingLocalTestingModeResult]())
+ c.commandParamsSchemas["Storage.setAttributionReportingTracking"] = abxjsonschema.SchemaFor[StorageSetAttributionReportingTrackingParams]()
+ c.commandResultSchemas["Storage.setAttributionReportingTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetAttributionReportingTrackingResult]())
+ c.commandParamsSchemas["Storage.sendPendingAttributionReports"] = abxjsonschema.SchemaFor[StorageSendPendingAttributionReportsParams]()
+ c.commandResultSchemas["Storage.sendPendingAttributionReports"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSendPendingAttributionReportsResult]())
+ c.commandParamsSchemas["Storage.getRelatedWebsiteSets"] = abxjsonschema.SchemaFor[StorageGetRelatedWebsiteSetsParams]()
+ c.commandResultSchemas["Storage.getRelatedWebsiteSets"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetRelatedWebsiteSetsResult]())
+ c.commandParamsSchemas["Storage.getAffectedUrlsForThirdPartyCookieMetadata"] = abxjsonschema.SchemaFor[StorageGetAffectedUrlsForThirdPartyCookieMetadataParams]()
+ c.commandResultSchemas["Storage.getAffectedUrlsForThirdPartyCookieMetadata"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetAffectedUrlsForThirdPartyCookieMetadataResult]())
+ c.commandParamsSchemas["Storage.setProtectedAudienceKAnonymity"] = abxjsonschema.SchemaFor[StorageSetProtectedAudienceKAnonymityParams]()
+ c.commandResultSchemas["Storage.setProtectedAudienceKAnonymity"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetProtectedAudienceKAnonymityResult]())
+ c.commandParamsSchemas["SystemInfo.getInfo"] = abxjsonschema.SchemaFor[SystemInfoGetInfoParams]()
+ c.commandResultSchemas["SystemInfo.getInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[SystemInfoGetInfoResult]())
+ c.commandParamsSchemas["SystemInfo.getFeatureState"] = abxjsonschema.SchemaFor[SystemInfoGetFeatureStateParams]()
+ c.commandResultSchemas["SystemInfo.getFeatureState"] = nativeResultSchema(abxjsonschema.SchemaFor[SystemInfoGetFeatureStateResult]())
+ c.commandParamsSchemas["SystemInfo.getProcessInfo"] = abxjsonschema.SchemaFor[SystemInfoGetProcessInfoParams]()
+ c.commandResultSchemas["SystemInfo.getProcessInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[SystemInfoGetProcessInfoResult]())
+ c.commandParamsSchemas["Target.activateTarget"] = abxjsonschema.SchemaFor[TargetActivateTargetParams]()
+ c.commandResultSchemas["Target.activateTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetActivateTargetResult]())
+ c.commandParamsSchemas["Target.attachToTarget"] = abxjsonschema.SchemaFor[TargetAttachToTargetParams]()
+ c.commandResultSchemas["Target.attachToTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetAttachToTargetResult]())
+ c.commandParamsSchemas["Target.attachToBrowserTarget"] = abxjsonschema.SchemaFor[TargetAttachToBrowserTargetParams]()
+ c.commandResultSchemas["Target.attachToBrowserTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetAttachToBrowserTargetResult]())
+ c.commandParamsSchemas["Target.closeTarget"] = abxjsonschema.SchemaFor[TargetCloseTargetParams]()
+ c.commandResultSchemas["Target.closeTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetCloseTargetResult]())
+ c.commandParamsSchemas["Target.exposeDevToolsProtocol"] = abxjsonschema.SchemaFor[TargetExposeDevToolsProtocolParams]()
+ c.commandResultSchemas["Target.exposeDevToolsProtocol"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetExposeDevToolsProtocolResult]())
+ c.commandParamsSchemas["Target.createBrowserContext"] = abxjsonschema.SchemaFor[TargetCreateBrowserContextParams]()
+ c.commandResultSchemas["Target.createBrowserContext"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetCreateBrowserContextResult]())
+ c.commandParamsSchemas["Target.getBrowserContexts"] = abxjsonschema.SchemaFor[TargetGetBrowserContextsParams]()
+ c.commandResultSchemas["Target.getBrowserContexts"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetGetBrowserContextsResult]())
+ c.commandParamsSchemas["Target.createTarget"] = abxjsonschema.SchemaFor[TargetCreateTargetParams]()
+ c.commandResultSchemas["Target.createTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetCreateTargetResult]())
+ c.commandParamsSchemas["Target.detachFromTarget"] = abxjsonschema.SchemaFor[TargetDetachFromTargetParams]()
+ c.commandResultSchemas["Target.detachFromTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetDetachFromTargetResult]())
+ c.commandParamsSchemas["Target.disposeBrowserContext"] = abxjsonschema.SchemaFor[TargetDisposeBrowserContextParams]()
+ c.commandResultSchemas["Target.disposeBrowserContext"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetDisposeBrowserContextResult]())
+ c.commandParamsSchemas["Target.getTargetInfo"] = abxjsonschema.SchemaFor[TargetGetTargetInfoParams]()
+ c.commandResultSchemas["Target.getTargetInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetGetTargetInfoResult]())
+ c.commandParamsSchemas["Target.getTargets"] = abxjsonschema.SchemaFor[TargetGetTargetsParams]()
+ c.commandResultSchemas["Target.getTargets"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetGetTargetsResult]())
+ c.commandParamsSchemas["Target.sendMessageToTarget"] = abxjsonschema.SchemaFor[TargetSendMessageToTargetParams]()
+ c.commandResultSchemas["Target.sendMessageToTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetSendMessageToTargetResult]())
+ c.commandParamsSchemas["Target.setAutoAttach"] = abxjsonschema.SchemaFor[TargetSetAutoAttachParams]()
+ c.commandResultSchemas["Target.setAutoAttach"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetSetAutoAttachResult]())
+ c.commandParamsSchemas["Target.autoAttachRelated"] = abxjsonschema.SchemaFor[TargetAutoAttachRelatedParams]()
+ c.commandResultSchemas["Target.autoAttachRelated"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetAutoAttachRelatedResult]())
+ c.commandParamsSchemas["Target.setDiscoverTargets"] = abxjsonschema.SchemaFor[TargetSetDiscoverTargetsParams]()
+ c.commandResultSchemas["Target.setDiscoverTargets"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetSetDiscoverTargetsResult]())
+ c.commandParamsSchemas["Target.setRemoteLocations"] = abxjsonschema.SchemaFor[TargetSetRemoteLocationsParams]()
+ c.commandResultSchemas["Target.setRemoteLocations"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetSetRemoteLocationsResult]())
+ c.commandParamsSchemas["Target.getDevToolsTarget"] = abxjsonschema.SchemaFor[TargetGetDevToolsTargetParams]()
+ c.commandResultSchemas["Target.getDevToolsTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetGetDevToolsTargetResult]())
+ c.commandParamsSchemas["Target.openDevTools"] = abxjsonschema.SchemaFor[TargetOpenDevToolsParams]()
+ c.commandResultSchemas["Target.openDevTools"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetOpenDevToolsResult]())
+ c.commandParamsSchemas["Tethering.bind"] = abxjsonschema.SchemaFor[TetheringBindParams]()
+ c.commandResultSchemas["Tethering.bind"] = nativeResultSchema(abxjsonschema.SchemaFor[TetheringBindResult]())
+ c.commandParamsSchemas["Tethering.unbind"] = abxjsonschema.SchemaFor[TetheringUnbindParams]()
+ c.commandResultSchemas["Tethering.unbind"] = nativeResultSchema(abxjsonschema.SchemaFor[TetheringUnbindResult]())
+ c.commandParamsSchemas["Tracing.end"] = abxjsonschema.SchemaFor[TracingEndParams]()
+ c.commandResultSchemas["Tracing.end"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingEndResult]())
+ c.commandParamsSchemas["Tracing.getCategories"] = abxjsonschema.SchemaFor[TracingGetCategoriesParams]()
+ c.commandResultSchemas["Tracing.getCategories"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingGetCategoriesResult]())
+ c.commandParamsSchemas["Tracing.getTrackEventDescriptor"] = abxjsonschema.SchemaFor[TracingGetTrackEventDescriptorParams]()
+ c.commandResultSchemas["Tracing.getTrackEventDescriptor"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingGetTrackEventDescriptorResult]())
+ c.commandParamsSchemas["Tracing.recordClockSyncMarker"] = abxjsonschema.SchemaFor[TracingRecordClockSyncMarkerParams]()
+ c.commandResultSchemas["Tracing.recordClockSyncMarker"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingRecordClockSyncMarkerResult]())
+ c.commandParamsSchemas["Tracing.requestMemoryDump"] = abxjsonschema.SchemaFor[TracingRequestMemoryDumpParams]()
+ c.commandResultSchemas["Tracing.requestMemoryDump"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingRequestMemoryDumpResult]())
+ c.commandParamsSchemas["Tracing.start"] = abxjsonschema.SchemaFor[TracingStartParams]()
+ c.commandResultSchemas["Tracing.start"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingStartResult]())
+ c.commandParamsSchemas["WebAudio.enable"] = abxjsonschema.SchemaFor[WebAudioEnableParams]()
+ c.commandResultSchemas["WebAudio.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioEnableResult]())
+ c.commandParamsSchemas["WebAudio.disable"] = abxjsonschema.SchemaFor[WebAudioDisableParams]()
+ c.commandResultSchemas["WebAudio.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioDisableResult]())
+ c.commandParamsSchemas["WebAudio.getRealtimeData"] = abxjsonschema.SchemaFor[WebAudioGetRealtimeDataParams]()
+ c.commandResultSchemas["WebAudio.getRealtimeData"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioGetRealtimeDataResult]())
+ c.commandParamsSchemas["WebAuthn.enable"] = abxjsonschema.SchemaFor[WebAuthnEnableParams]()
+ c.commandResultSchemas["WebAuthn.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnEnableResult]())
+ c.commandParamsSchemas["WebAuthn.disable"] = abxjsonschema.SchemaFor[WebAuthnDisableParams]()
+ c.commandResultSchemas["WebAuthn.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnDisableResult]())
+ c.commandParamsSchemas["WebAuthn.addVirtualAuthenticator"] = abxjsonschema.SchemaFor[WebAuthnAddVirtualAuthenticatorParams]()
+ c.commandResultSchemas["WebAuthn.addVirtualAuthenticator"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnAddVirtualAuthenticatorResult]())
+ c.commandParamsSchemas["WebAuthn.setResponseOverrideBits"] = abxjsonschema.SchemaFor[WebAuthnSetResponseOverrideBitsParams]()
+ c.commandResultSchemas["WebAuthn.setResponseOverrideBits"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnSetResponseOverrideBitsResult]())
+ c.commandParamsSchemas["WebAuthn.removeVirtualAuthenticator"] = abxjsonschema.SchemaFor[WebAuthnRemoveVirtualAuthenticatorParams]()
+ c.commandResultSchemas["WebAuthn.removeVirtualAuthenticator"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnRemoveVirtualAuthenticatorResult]())
+ c.commandParamsSchemas["WebAuthn.addCredential"] = abxjsonschema.SchemaFor[WebAuthnAddCredentialParams]()
+ c.commandResultSchemas["WebAuthn.addCredential"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnAddCredentialResult]())
+ c.commandParamsSchemas["WebAuthn.getCredential"] = abxjsonschema.SchemaFor[WebAuthnGetCredentialParams]()
+ c.commandResultSchemas["WebAuthn.getCredential"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnGetCredentialResult]())
+ c.commandParamsSchemas["WebAuthn.getCredentials"] = abxjsonschema.SchemaFor[WebAuthnGetCredentialsParams]()
+ c.commandResultSchemas["WebAuthn.getCredentials"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnGetCredentialsResult]())
+ c.commandParamsSchemas["WebAuthn.removeCredential"] = abxjsonschema.SchemaFor[WebAuthnRemoveCredentialParams]()
+ c.commandResultSchemas["WebAuthn.removeCredential"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnRemoveCredentialResult]())
+ c.commandParamsSchemas["WebAuthn.clearCredentials"] = abxjsonschema.SchemaFor[WebAuthnClearCredentialsParams]()
+ c.commandResultSchemas["WebAuthn.clearCredentials"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnClearCredentialsResult]())
+ c.commandParamsSchemas["WebAuthn.setUserVerified"] = abxjsonschema.SchemaFor[WebAuthnSetUserVerifiedParams]()
+ c.commandResultSchemas["WebAuthn.setUserVerified"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnSetUserVerifiedResult]())
+ c.commandParamsSchemas["WebAuthn.setAutomaticPresenceSimulation"] = abxjsonschema.SchemaFor[WebAuthnSetAutomaticPresenceSimulationParams]()
+ c.commandResultSchemas["WebAuthn.setAutomaticPresenceSimulation"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnSetAutomaticPresenceSimulationResult]())
+ c.commandParamsSchemas["WebAuthn.setCredentialProperties"] = abxjsonschema.SchemaFor[WebAuthnSetCredentialPropertiesParams]()
+ c.commandResultSchemas["WebAuthn.setCredentialProperties"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnSetCredentialPropertiesResult]())
+ c.eventSchemas["Accessibility.loadComplete"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityLoadCompleteEvent]())
+ c.eventSchemas["Accessibility.nodesUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityNodesUpdatedEvent]())
+ c.eventSchemas["Animation.animationCanceled"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationAnimationCanceledEvent]())
+ c.eventSchemas["Animation.animationCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationAnimationCreatedEvent]())
+ c.eventSchemas["Animation.animationStarted"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationAnimationStartedEvent]())
+ c.eventSchemas["Animation.animationUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationAnimationUpdatedEvent]())
+ c.eventSchemas["Audits.issueAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsIssueAddedEvent]())
+ c.eventSchemas["Autofill.addressFormFilled"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillAddressFormFilledEvent]())
+ c.eventSchemas["BackgroundService.recordingStateChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceRecordingStateChangedEvent]())
+ c.eventSchemas["BackgroundService.backgroundServiceEventReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceBackgroundServiceEventReceivedEvent]())
+ c.eventSchemas["BluetoothEmulation.gattOperationReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationGattOperationReceivedEvent]())
+ c.eventSchemas["BluetoothEmulation.characteristicOperationReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationCharacteristicOperationReceivedEvent]())
+ c.eventSchemas["BluetoothEmulation.descriptorOperationReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationDescriptorOperationReceivedEvent]())
+ c.eventSchemas["Browser.downloadWillBegin"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserDownloadWillBeginEvent]())
+ c.eventSchemas["Browser.downloadProgress"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserDownloadProgressEvent]())
+ c.eventSchemas["CSS.fontsUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSFontsUpdatedEvent]())
+ c.eventSchemas["CSS.mediaQueryResultChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSMediaQueryResultChangedEvent]())
+ c.eventSchemas["CSS.styleSheetAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStyleSheetAddedEvent]())
+ c.eventSchemas["CSS.styleSheetChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStyleSheetChangedEvent]())
+ c.eventSchemas["CSS.styleSheetRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStyleSheetRemovedEvent]())
+ c.eventSchemas["CSS.computedStyleUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSComputedStyleUpdatedEvent]())
+ c.eventSchemas["Cast.sinksUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[CastSinksUpdatedEvent]())
+ c.eventSchemas["Cast.issueUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[CastIssueUpdatedEvent]())
+ c.eventSchemas["Console.messageAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[ConsoleMessageAddedEvent]())
+ c.eventSchemas["DOM.attributeModified"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAttributeModifiedEvent]())
+ c.eventSchemas["DOM.adoptedStyleSheetsModified"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAdoptedStyleSheetsModifiedEvent]())
+ c.eventSchemas["DOM.attributeRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAttributeRemovedEvent]())
+ c.eventSchemas["DOM.characterDataModified"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMCharacterDataModifiedEvent]())
+ c.eventSchemas["DOM.childNodeCountUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMChildNodeCountUpdatedEvent]())
+ c.eventSchemas["DOM.childNodeInserted"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMChildNodeInsertedEvent]())
+ c.eventSchemas["DOM.childNodeRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMChildNodeRemovedEvent]())
+ c.eventSchemas["DOM.distributedNodesUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDistributedNodesUpdatedEvent]())
+ c.eventSchemas["DOM.documentUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDocumentUpdatedEvent]())
+ c.eventSchemas["DOM.inlineStyleInvalidated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMInlineStyleInvalidatedEvent]())
+ c.eventSchemas["DOM.pseudoElementAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPseudoElementAddedEvent]())
+ c.eventSchemas["DOM.topLayerElementsUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMTopLayerElementsUpdatedEvent]())
+ c.eventSchemas["DOM.scrollableFlagUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMScrollableFlagUpdatedEvent]())
+ c.eventSchemas["DOM.adRelatedStateUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAdRelatedStateUpdatedEvent]())
+ c.eventSchemas["DOM.affectedByStartingStylesFlagUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAffectedByStartingStylesFlagUpdatedEvent]())
+ c.eventSchemas["DOM.pseudoElementRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPseudoElementRemovedEvent]())
+ c.eventSchemas["DOM.setChildNodes"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetChildNodesEvent]())
+ c.eventSchemas["DOM.shadowRootPopped"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMShadowRootPoppedEvent]())
+ c.eventSchemas["DOM.shadowRootPushed"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMShadowRootPushedEvent]())
+ c.eventSchemas["DOMStorage.domStorageItemAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDOMStorageItemAddedEvent]())
+ c.eventSchemas["DOMStorage.domStorageItemRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDOMStorageItemRemovedEvent]())
+ c.eventSchemas["DOMStorage.domStorageItemUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDOMStorageItemUpdatedEvent]())
+ c.eventSchemas["DOMStorage.domStorageItemsCleared"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDOMStorageItemsClearedEvent]())
+ c.eventSchemas["Debugger.breakpointResolved"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerBreakpointResolvedEvent]())
+ c.eventSchemas["Debugger.paused"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerPausedEvent]())
+ c.eventSchemas["Debugger.resumed"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerResumedEvent]())
+ c.eventSchemas["Debugger.scriptFailedToParse"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerScriptFailedToParseEvent]())
+ c.eventSchemas["Debugger.scriptParsed"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerScriptParsedEvent]())
+ c.eventSchemas["DeviceAccess.deviceRequestPrompted"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessDeviceRequestPromptedEvent]())
+ c.eventSchemas["Emulation.virtualTimeBudgetExpired"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationVirtualTimeBudgetExpiredEvent]())
+ c.eventSchemas["Emulation.screenOrientationLockChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationScreenOrientationLockChangedEvent]())
+ c.eventSchemas["FedCm.dialogShown"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmDialogShownEvent]())
+ c.eventSchemas["FedCm.dialogClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmDialogClosedEvent]())
+ c.eventSchemas["Fetch.requestPaused"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchRequestPausedEvent]())
+ c.eventSchemas["Fetch.authRequired"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchAuthRequiredEvent]())
+ c.eventSchemas["HeapProfiler.addHeapSnapshotChunk"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerAddHeapSnapshotChunkEvent]())
+ c.eventSchemas["HeapProfiler.heapStatsUpdate"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerHeapStatsUpdateEvent]())
+ c.eventSchemas["HeapProfiler.lastSeenObjectId"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerLastSeenObjectIDEvent]())
+ c.eventSchemas["HeapProfiler.reportHeapSnapshotProgress"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerReportHeapSnapshotProgressEvent]())
+ c.eventSchemas["HeapProfiler.resetProfiles"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerResetProfilesEvent]())
+ c.eventSchemas["Input.dragIntercepted"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDragInterceptedEvent]())
+ c.eventSchemas["Inspector.detached"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorDetachedEvent]())
+ c.eventSchemas["Inspector.targetCrashed"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorTargetCrashedEvent]())
+ c.eventSchemas["Inspector.targetReloadedAfterCrash"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorTargetReloadedAfterCrashEvent]())
+ c.eventSchemas["Inspector.workerScriptLoaded"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorWorkerScriptLoadedEvent]())
+ c.eventSchemas["LayerTree.layerPainted"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeLayerPaintedEvent]())
+ c.eventSchemas["LayerTree.layerTreeDidChange"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeLayerTreeDidChangeEvent]())
+ c.eventSchemas["Log.entryAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[LogEntryAddedEvent]())
+ c.eventSchemas["Media.playerPropertiesChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerPropertiesChangedEvent]())
+ c.eventSchemas["Media.playerEventsAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerEventsAddedEvent]())
+ c.eventSchemas["Media.playerMessagesLogged"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerMessagesLoggedEvent]())
+ c.eventSchemas["Media.playerErrorsRaised"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerErrorsRaisedEvent]())
+ c.eventSchemas["Media.playerCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerCreatedEvent]())
+ c.eventSchemas["Network.dataReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDataReceivedEvent]())
+ c.eventSchemas["Network.eventSourceMessageReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEventSourceMessageReceivedEvent]())
+ c.eventSchemas["Network.loadingFailed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkLoadingFailedEvent]())
+ c.eventSchemas["Network.loadingFinished"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkLoadingFinishedEvent]())
+ c.eventSchemas["Network.requestIntercepted"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkRequestInterceptedEvent]())
+ c.eventSchemas["Network.requestServedFromCache"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkRequestServedFromCacheEvent]())
+ c.eventSchemas["Network.requestWillBeSent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkRequestWillBeSentEvent]())
+ c.eventSchemas["Network.resourceChangedPriority"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkResourceChangedPriorityEvent]())
+ c.eventSchemas["Network.signedExchangeReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSignedExchangeReceivedEvent]())
+ c.eventSchemas["Network.responseReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkResponseReceivedEvent]())
+ c.eventSchemas["Network.webSocketClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketClosedEvent]())
+ c.eventSchemas["Network.webSocketCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketCreatedEvent]())
+ c.eventSchemas["Network.webSocketFrameError"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketFrameErrorEvent]())
+ c.eventSchemas["Network.webSocketFrameReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketFrameReceivedEvent]())
+ c.eventSchemas["Network.webSocketFrameSent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketFrameSentEvent]())
+ c.eventSchemas["Network.webSocketHandshakeResponseReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketHandshakeResponseReceivedEvent]())
+ c.eventSchemas["Network.webSocketWillSendHandshakeRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketWillSendHandshakeRequestEvent]())
+ c.eventSchemas["Network.webTransportCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebTransportCreatedEvent]())
+ c.eventSchemas["Network.webTransportConnectionEstablished"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebTransportConnectionEstablishedEvent]())
+ c.eventSchemas["Network.webTransportClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebTransportClosedEvent]())
+ c.eventSchemas["Network.directTCPSocketCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketCreatedEvent]())
+ c.eventSchemas["Network.directTCPSocketOpened"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketOpenedEvent]())
+ c.eventSchemas["Network.directTCPSocketAborted"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketAbortedEvent]())
+ c.eventSchemas["Network.directTCPSocketClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketClosedEvent]())
+ c.eventSchemas["Network.directTCPSocketChunkSent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketChunkSentEvent]())
+ c.eventSchemas["Network.directTCPSocketChunkReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketChunkReceivedEvent]())
+ c.eventSchemas["Network.directUDPSocketJoinedMulticastGroup"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketJoinedMulticastGroupEvent]())
+ c.eventSchemas["Network.directUDPSocketLeftMulticastGroup"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketLeftMulticastGroupEvent]())
+ c.eventSchemas["Network.directUDPSocketCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketCreatedEvent]())
+ c.eventSchemas["Network.directUDPSocketOpened"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketOpenedEvent]())
+ c.eventSchemas["Network.directUDPSocketAborted"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketAbortedEvent]())
+ c.eventSchemas["Network.directUDPSocketClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketClosedEvent]())
+ c.eventSchemas["Network.directUDPSocketChunkSent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketChunkSentEvent]())
+ c.eventSchemas["Network.directUDPSocketChunkReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketChunkReceivedEvent]())
+ c.eventSchemas["Network.requestWillBeSentExtraInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkRequestWillBeSentExtraInfoEvent]())
+ c.eventSchemas["Network.responseReceivedExtraInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkResponseReceivedExtraInfoEvent]())
+ c.eventSchemas["Network.responseReceivedEarlyHints"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkResponseReceivedEarlyHintsEvent]())
+ c.eventSchemas["Network.trustTokenOperationDone"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkTrustTokenOperationDoneEvent]())
+ c.eventSchemas["Network.policyUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkPolicyUpdatedEvent]())
+ c.eventSchemas["Network.reportingApiReportAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkReportingAPIReportAddedEvent]())
+ c.eventSchemas["Network.reportingApiReportUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkReportingAPIReportUpdatedEvent]())
+ c.eventSchemas["Network.reportingApiEndpointsChangedForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkReportingAPIEndpointsChangedForOriginEvent]())
+ c.eventSchemas["Network.deviceBoundSessionsAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDeviceBoundSessionsAddedEvent]())
+ c.eventSchemas["Network.deviceBoundSessionEventOccurred"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDeviceBoundSessionEventOccurredEvent]())
+ c.eventSchemas["Overlay.inspectNodeRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayInspectNodeRequestedEvent]())
+ c.eventSchemas["Overlay.nodeHighlightRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayNodeHighlightRequestedEvent]())
+ c.eventSchemas["Overlay.screenshotRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayScreenshotRequestedEvent]())
+ c.eventSchemas["Overlay.inspectPanelShowRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayInspectPanelShowRequestedEvent]())
+ c.eventSchemas["Overlay.inspectedElementWindowRestored"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayInspectedElementWindowRestoredEvent]())
+ c.eventSchemas["Overlay.inspectModeCanceled"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayInspectModeCanceledEvent]())
+ c.eventSchemas["Page.domContentEventFired"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDOMContentEventFiredEvent]())
+ c.eventSchemas["Page.fileChooserOpened"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFileChooserOpenedEvent]())
+ c.eventSchemas["Page.frameAttached"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameAttachedEvent]())
+ c.eventSchemas["Page.frameClearedScheduledNavigation"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameClearedScheduledNavigationEvent]())
+ c.eventSchemas["Page.frameDetached"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameDetachedEvent]())
+ c.eventSchemas["Page.frameSubtreeWillBeDetached"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameSubtreeWillBeDetachedEvent]())
+ c.eventSchemas["Page.frameNavigated"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameNavigatedEvent]())
+ c.eventSchemas["Page.documentOpened"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDocumentOpenedEvent]())
+ c.eventSchemas["Page.frameResized"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameResizedEvent]())
+ c.eventSchemas["Page.frameStartedNavigating"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameStartedNavigatingEvent]())
+ c.eventSchemas["Page.frameRequestedNavigation"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameRequestedNavigationEvent]())
+ c.eventSchemas["Page.frameScheduledNavigation"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameScheduledNavigationEvent]())
+ c.eventSchemas["Page.frameStartedLoading"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameStartedLoadingEvent]())
+ c.eventSchemas["Page.frameStoppedLoading"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameStoppedLoadingEvent]())
+ c.eventSchemas["Page.downloadWillBegin"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDownloadWillBeginEvent]())
+ c.eventSchemas["Page.downloadProgress"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDownloadProgressEvent]())
+ c.eventSchemas["Page.interstitialHidden"] = nativeResultSchema(abxjsonschema.SchemaFor[PageInterstitialHiddenEvent]())
+ c.eventSchemas["Page.interstitialShown"] = nativeResultSchema(abxjsonschema.SchemaFor[PageInterstitialShownEvent]())
+ c.eventSchemas["Page.javascriptDialogClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[PageJavascriptDialogClosedEvent]())
+ c.eventSchemas["Page.javascriptDialogOpening"] = nativeResultSchema(abxjsonschema.SchemaFor[PageJavascriptDialogOpeningEvent]())
+ c.eventSchemas["Page.lifecycleEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[PageLifecycleEventEvent]())
+ c.eventSchemas["Page.backForwardCacheNotUsed"] = nativeResultSchema(abxjsonschema.SchemaFor[PageBackForwardCacheNotUsedEvent]())
+ c.eventSchemas["Page.loadEventFired"] = nativeResultSchema(abxjsonschema.SchemaFor[PageLoadEventFiredEvent]())
+ c.eventSchemas["Page.navigatedWithinDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[PageNavigatedWithinDocumentEvent]())
+ c.eventSchemas["Page.screencastFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[PageScreencastFrameEvent]())
+ c.eventSchemas["Page.screencastVisibilityChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[PageScreencastVisibilityChangedEvent]())
+ c.eventSchemas["Page.windowOpen"] = nativeResultSchema(abxjsonschema.SchemaFor[PageWindowOpenEvent]())
+ c.eventSchemas["Page.compilationCacheProduced"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCompilationCacheProducedEvent]())
+ c.eventSchemas["Performance.metrics"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceMetricsEvent]())
+ c.eventSchemas["PerformanceTimeline.timelineEventAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceTimelineTimelineEventAddedEvent]())
+ c.eventSchemas["Preload.ruleSetUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadRuleSetUpdatedEvent]())
+ c.eventSchemas["Preload.ruleSetRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadRuleSetRemovedEvent]())
+ c.eventSchemas["Preload.preloadEnabledStateUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadPreloadEnabledStateUpdatedEvent]())
+ c.eventSchemas["Preload.prefetchStatusUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadPrefetchStatusUpdatedEvent]())
+ c.eventSchemas["Preload.prerenderStatusUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadPrerenderStatusUpdatedEvent]())
+ c.eventSchemas["Preload.preloadingAttemptSourcesUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadPreloadingAttemptSourcesUpdatedEvent]())
+ c.eventSchemas["Profiler.consoleProfileFinished"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerConsoleProfileFinishedEvent]())
+ c.eventSchemas["Profiler.consoleProfileStarted"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerConsoleProfileStartedEvent]())
+ c.eventSchemas["Profiler.preciseCoverageDeltaUpdate"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerPreciseCoverageDeltaUpdateEvent]())
+ c.eventSchemas["Runtime.bindingCalled"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeBindingCalledEvent]())
+ c.eventSchemas["Runtime.consoleAPICalled"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeConsoleAPICalledEvent]())
+ c.eventSchemas["Runtime.exceptionRevoked"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExceptionRevokedEvent]())
+ c.eventSchemas["Runtime.exceptionThrown"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExceptionThrownEvent]())
+ c.eventSchemas["Runtime.executionContextCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExecutionContextCreatedEvent]())
+ c.eventSchemas["Runtime.executionContextDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExecutionContextDestroyedEvent]())
+ c.eventSchemas["Runtime.executionContextsCleared"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExecutionContextsClearedEvent]())
+ c.eventSchemas["Runtime.inspectRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeInspectRequestedEvent]())
+ c.eventSchemas["Security.certificateError"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityCertificateErrorEvent]())
+ c.eventSchemas["Security.visibleSecurityStateChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityVisibleSecurityStateChangedEvent]())
+ c.eventSchemas["Security.securityStateChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[SecuritySecurityStateChangedEvent]())
+ c.eventSchemas["ServiceWorker.workerErrorReported"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerWorkerErrorReportedEvent]())
+ c.eventSchemas["ServiceWorker.workerRegistrationUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerWorkerRegistrationUpdatedEvent]())
+ c.eventSchemas["ServiceWorker.workerVersionUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerWorkerVersionUpdatedEvent]())
+ c.eventSchemas["SmartCardEmulation.establishContextRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationEstablishContextRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.releaseContextRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReleaseContextRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.listReadersRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationListReadersRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.getStatusChangeRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationGetStatusChangeRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.cancelRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationCancelRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.connectRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationConnectRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.disconnectRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationDisconnectRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.transmitRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationTransmitRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.controlRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationControlRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.getAttribRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationGetAttribRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.setAttribRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationSetAttribRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.statusRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationStatusRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.beginTransactionRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationBeginTransactionRequestedEvent]())
+ c.eventSchemas["SmartCardEmulation.endTransactionRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationEndTransactionRequestedEvent]())
+ c.eventSchemas["Storage.cacheStorageContentUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageCacheStorageContentUpdatedEvent]())
+ c.eventSchemas["Storage.cacheStorageListUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageCacheStorageListUpdatedEvent]())
+ c.eventSchemas["Storage.indexedDBContentUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageIndexedDBContentUpdatedEvent]())
+ c.eventSchemas["Storage.indexedDBListUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageIndexedDBListUpdatedEvent]())
+ c.eventSchemas["Storage.interestGroupAccessed"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageInterestGroupAccessedEvent]())
+ c.eventSchemas["Storage.interestGroupAuctionEventOccurred"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageInterestGroupAuctionEventOccurredEvent]())
+ c.eventSchemas["Storage.interestGroupAuctionNetworkRequestCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageInterestGroupAuctionNetworkRequestCreatedEvent]())
+ c.eventSchemas["Storage.sharedStorageAccessed"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSharedStorageAccessedEvent]())
+ c.eventSchemas["Storage.sharedStorageWorkletOperationExecutionFinished"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSharedStorageWorkletOperationExecutionFinishedEvent]())
+ c.eventSchemas["Storage.storageBucketCreatedOrUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageStorageBucketCreatedOrUpdatedEvent]())
+ c.eventSchemas["Storage.storageBucketDeleted"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageStorageBucketDeletedEvent]())
+ c.eventSchemas["Storage.attributionReportingSourceRegistered"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageAttributionReportingSourceRegisteredEvent]())
+ c.eventSchemas["Storage.attributionReportingTriggerRegistered"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageAttributionReportingTriggerRegisteredEvent]())
+ c.eventSchemas["Storage.attributionReportingReportSent"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageAttributionReportingReportSentEvent]())
+ c.eventSchemas["Storage.attributionReportingVerboseDebugReportSent"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageAttributionReportingVerboseDebugReportSentEvent]())
+ c.eventSchemas["Target.attachedToTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetAttachedToTargetEvent]())
+ c.eventSchemas["Target.detachedFromTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetDetachedFromTargetEvent]())
+ c.eventSchemas["Target.receivedMessageFromTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetReceivedMessageFromTargetEvent]())
+ c.eventSchemas["Target.targetCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetTargetCreatedEvent]())
+ c.eventSchemas["Target.targetDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetTargetDestroyedEvent]())
+ c.eventSchemas["Target.targetCrashed"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetTargetCrashedEvent]())
+ c.eventSchemas["Target.targetInfoChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetTargetInfoChangedEvent]())
+ c.eventSchemas["Tethering.accepted"] = nativeResultSchema(abxjsonschema.SchemaFor[TetheringAcceptedEvent]())
+ c.eventSchemas["Tracing.bufferUsage"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingBufferUsageEvent]())
+ c.eventSchemas["Tracing.dataCollected"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingDataCollectedEvent]())
+ c.eventSchemas["Tracing.tracingComplete"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingTracingCompleteEvent]())
+ c.eventSchemas["WebAudio.contextCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioContextCreatedEvent]())
+ c.eventSchemas["WebAudio.contextWillBeDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioContextWillBeDestroyedEvent]())
+ c.eventSchemas["WebAudio.contextChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioContextChangedEvent]())
+ c.eventSchemas["WebAudio.audioListenerCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioListenerCreatedEvent]())
+ c.eventSchemas["WebAudio.audioListenerWillBeDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioListenerWillBeDestroyedEvent]())
+ c.eventSchemas["WebAudio.audioNodeCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioNodeCreatedEvent]())
+ c.eventSchemas["WebAudio.audioNodeWillBeDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioNodeWillBeDestroyedEvent]())
+ c.eventSchemas["WebAudio.audioParamCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioParamCreatedEvent]())
+ c.eventSchemas["WebAudio.audioParamWillBeDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioParamWillBeDestroyedEvent]())
+ c.eventSchemas["WebAudio.nodesConnected"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioNodesConnectedEvent]())
+ c.eventSchemas["WebAudio.nodesDisconnected"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioNodesDisconnectedEvent]())
+ c.eventSchemas["WebAudio.nodeParamConnected"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioNodeParamConnectedEvent]())
+ c.eventSchemas["WebAudio.nodeParamDisconnected"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioNodeParamDisconnectedEvent]())
+ c.eventSchemas["WebAuthn.credentialAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnCredentialAddedEvent]())
+ c.eventSchemas["WebAuthn.credentialDeleted"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnCredentialDeletedEvent]())
+ c.eventSchemas["WebAuthn.credentialUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnCredentialUpdatedEvent]())
+ c.eventSchemas["WebAuthn.credentialAsserted"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnCredentialAssertedEvent]())
+}
diff --git a/go/modcdp/injector/BorrowedExtensionInjector.go b/go/modcdp/injector/BorrowedExtensionInjector.go
index e82a852..c6af6ec 100644
--- a/go/modcdp/injector/BorrowedExtensionInjector.go
+++ b/go/modcdp/injector/BorrowedExtensionInjector.go
@@ -166,9 +166,11 @@ func modcdpServerPathFromExtensionPath(extensionPath string) (string, error) {
if extensionPath != "" {
candidates = append(candidates, filepath.Join(extensionPath, "ModCDPServer.js"))
candidates = append(candidates, filepath.Join(extensionPath, "js", "src", "server", "ModCDPServer.js"))
+ candidates = append(candidates, filepath.Join(filepath.Dir(extensionPath), "js", "src", "server", "ModCDPServer.js"))
}
if _, file, _, ok := runtime.Caller(0); ok {
for dir := filepath.Dir(file); ; dir = filepath.Dir(dir) {
+ candidates = append(candidates, filepath.Join(dir, "dist", "js", "src", "server", "ModCDPServer.js"))
candidates = append(candidates, filepath.Join(dir, "dist", "extension", "js", "src", "server", "ModCDPServer.js"))
candidates = append(candidates, filepath.Join(dir, "dist", "extension", "ModCDPServer.js"))
if parent := filepath.Dir(dir); parent == dir {
diff --git a/go/modcdp/injector/BorrowedExtensionInjector_test.go b/go/modcdp/injector/BorrowedExtensionInjector_test.go
index 0dc5689..9441946 100644
--- a/go/modcdp/injector/BorrowedExtensionInjector_test.go
+++ b/go/modcdp/injector/BorrowedExtensionInjector_test.go
@@ -12,19 +12,25 @@ func TestBorrowedExtensionInjectorBootstrapsModCDPInsideLiveExtensionServiceWork
if err != nil {
t.Fatal(err)
}
- chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
- Headless: boolPtr(true),
- Sandbox: boolPtr(false),
- ExtraArgs: []string{"--load-extension=" + extensionPath},
- }).Launch(modcdp.LaunchOptions{})
- if err != nil {
+ headless := true
+ owner := modcdp.New(modcdp.Options{
+ Launcher: modcdp.LauncherConfig{LauncherMode: "local", LauncherOptions: modcdp.LaunchOptions{Headless: &headless}},
+ Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws"},
+ Injector: modcdp.InjectorConfig{
+ InjectorMode: "auto",
+ InjectorExtensionPath: extensionPath,
+ InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
+ InjectorTrustServiceWorkerTarget: true,
+ },
+ })
+ defer owner.Close()
+
+ if err := owner.Connect(); err != nil {
t.Fatal(err)
}
- defer chrome.Close()
-
cdp := modcdp.New(modcdp.Options{
Launcher: modcdp.LauncherConfig{LauncherMode: "remote"},
- Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL},
+ Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: owner.CDPURL},
Injector: modcdp.InjectorConfig{
InjectorMode: "borrow",
InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
@@ -42,12 +48,13 @@ func TestBorrowedExtensionInjectorBootstrapsModCDPInsideLiveExtensionServiceWork
if cdp.ExtensionID != DefaultModCDPExtensionID {
t.Fatalf("ExtensionID = %q", cdp.ExtensionID)
}
- result, err := cdp.Send("Target.getTargets", map[string]any{})
+ result, err := cdp.Mod.Evaluate(map[string]any{
+ "expression": "chrome.runtime.getURL('modcdp/service_worker.js')",
+ })
if err != nil {
t.Fatal(err)
}
- targetInfos, _ := result.(map[string]any)["targetInfos"].([]any)
- if len(targetInfos) == 0 {
- t.Fatalf("Target.getTargets = %#v", result)
+ if result != "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js" {
+ t.Fatalf("Mod.evaluate = %#v", result)
}
}
diff --git a/go/modcdp/injector/DiscoveredExtensionInjector.go b/go/modcdp/injector/DiscoveredExtensionInjector.go
index 886ad71..d83d144 100644
--- a/go/modcdp/injector/DiscoveredExtensionInjector.go
+++ b/go/modcdp/injector/DiscoveredExtensionInjector.go
@@ -30,15 +30,6 @@ func (i *DiscoveredExtensionInjector) Inject() (*ExtensionInjectionResult, error
return waited, err
}
}
- if i.wakeConfiguredExtension() {
- waited, err := i.waitForReadyServiceWorker(i.Options.InjectorServiceWorkerProbeTimeoutMS, i.Options.InjectorTrustServiceWorkerTarget)
- if err != nil || waited != nil {
- if waited != nil {
- waited.Source = "discovered"
- }
- return waited, err
- }
- }
if !i.Options.InjectorRequireServiceWorkerTarget {
return nil, nil
}
diff --git a/go/modcdp/injector/DiscoveredExtensionInjector_test.go b/go/modcdp/injector/DiscoveredExtensionInjector_test.go
index bc3db0b..b689838 100644
--- a/go/modcdp/injector/DiscoveredExtensionInjector_test.go
+++ b/go/modcdp/injector/DiscoveredExtensionInjector_test.go
@@ -1,35 +1,36 @@
package injector_test
import (
- "encoding/json"
modcdp "github.com/browserbase/modcdp/go/modcdp/client"
. "github.com/browserbase/modcdp/go/modcdp/injector"
- "os"
"path/filepath"
"testing"
)
-const customDiscoveredExtensionID = "hhklgmbgnbeghnjidampacgmgnhelifg"
-const customDiscoveredExtensionPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzG1LUbtH0aHMKjTAUeT0saY8xfnRNENctFJme3C1qnsqT7PAXMxJC4nT7tBZy2gEGRirBb3zIZ3OyAu9a0QR8lTLupDp4qHWOhQ7dl9ZjxjQdYa4Gby0xuXLdQrJIxDbmuv+UVJvYa8vRTwQB8koygbzDDDP5/YiB6mc0hbh8XBb82Ossy7T280k8280o/rS0CXdioUraCHj58PDhfxbs18TBcYfOjuRqua9J2oddxobtGehSD0gDtbvn2IWDtRajOlgZZyuS1vLoSR7C1ulFzpRSYPEMhI2x+wphut7E3QImyJ577YeULVGpt988FcixOou7udjx3/IUWjpq8046wIDAQAB"
-
func TestDiscoveredExtensionInjectorAttachesToAlreadyLoadedRealModCDPExtension(t *testing.T) {
extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
if err != nil {
t.Fatal(err)
}
- chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
- Headless: boolPtr(true),
- Sandbox: boolPtr(false),
- ExtraArgs: []string{"--load-extension=" + extensionPath},
- }).Launch(modcdp.LaunchOptions{})
- if err != nil {
+ headless := true
+ owner := modcdp.New(modcdp.Options{
+ Launcher: modcdp.LauncherConfig{LauncherMode: "local", LauncherOptions: modcdp.LaunchOptions{Headless: &headless}},
+ Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws"},
+ Injector: modcdp.InjectorConfig{
+ InjectorMode: "auto",
+ InjectorExtensionPath: extensionPath,
+ InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
+ InjectorTrustServiceWorkerTarget: true,
+ },
+ })
+ defer owner.Close()
+
+ if err := owner.Connect(); err != nil {
t.Fatal(err)
}
- defer chrome.Close()
-
cdp := modcdp.New(modcdp.Options{
Launcher: modcdp.LauncherConfig{LauncherMode: "remote"},
- Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL},
+ Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: owner.CDPURL},
Injector: modcdp.InjectorConfig{
InjectorMode: "discover",
InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
@@ -57,96 +58,3 @@ func TestDiscoveredExtensionInjectorAttachesToAlreadyLoadedRealModCDPExtension(t
t.Fatalf("Mod.evaluate = %#v", result)
}
}
-
-func TestDiscoveredExtensionInjectorSelectsConfiguredExtensionWhenMultipleModCDPWorkersExist(t *testing.T) {
- extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
- if err != nil {
- t.Fatal(err)
- }
- customExtensionPath := filepath.Join(t.TempDir(), "extension")
- if err := copyDir(extensionPath, customExtensionPath); err != nil {
- t.Fatal(err)
- }
- manifestPath := filepath.Join(customExtensionPath, "manifest.json")
- manifestBytes, err := os.ReadFile(manifestPath)
- if err != nil {
- t.Fatal(err)
- }
- var manifest map[string]any
- if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
- t.Fatal(err)
- }
- manifest["key"] = customDiscoveredExtensionPublicKey
- manifest["name"] = "ModCDP Bridge Custom Test"
- updatedManifest, err := json.MarshalIndent(manifest, "", " ")
- if err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(manifestPath, append(updatedManifest, '\n'), 0o644); err != nil {
- t.Fatal(err)
- }
-
- chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
- Headless: boolPtr(true),
- Sandbox: boolPtr(false),
- ExtraArgs: []string{"--load-extension=" + extensionPath + "," + customExtensionPath},
- }).Launch(modcdp.LaunchOptions{})
- if err != nil {
- t.Fatal(err)
- }
- defer chrome.Close()
-
- cdp := modcdp.New(modcdp.Options{
- Launcher: modcdp.LauncherConfig{LauncherMode: "remote"},
- Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL},
- Injector: modcdp.InjectorConfig{
- InjectorMode: "discover",
- InjectorExtensionID: customDiscoveredExtensionID,
- InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
- InjectorTrustServiceWorkerTarget: true,
- InjectorRequireServiceWorkerTarget: true,
- },
- })
- defer cdp.Close()
-
- if err := cdp.Connect(); err != nil {
- t.Fatal(err)
- }
- if cdp.ConnectTiming["injector_source"] != "discovered" {
- t.Fatalf("injector_source = %v", cdp.ConnectTiming["injector_source"])
- }
- if cdp.ExtensionID != customDiscoveredExtensionID {
- t.Fatalf("ExtensionID = %q", cdp.ExtensionID)
- }
- result, err := cdp.Mod.Evaluate(map[string]any{"expression": "chrome.runtime.id"})
- if err != nil {
- t.Fatal(err)
- }
- if result != customDiscoveredExtensionID {
- t.Fatalf("Mod.evaluate = %#v", result)
- }
-
- targetsResult, err := cdp.SendRaw("Target.getTargets", nil)
- if err != nil {
- t.Fatal(err)
- }
- targets, _ := targetsResult["targetInfos"].([]any)
- foundCustom := false
- foundDefault := false
- for _, rawTarget := range targets {
- target, _ := rawTarget.(map[string]any)
- if target["type"] != "service_worker" {
- continue
- }
- url, _ := target["url"].(string)
- switch url {
- case "chrome-extension://" + customDiscoveredExtensionID + "/modcdp/service_worker.js":
- foundCustom = true
- case "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js":
- foundDefault = true
- }
- }
- if !foundCustom || !foundDefault {
- t.Fatalf("expected custom and default ModCDP workers, custom=%v default=%v targets=%#v", foundCustom, foundDefault, targets)
- }
-}
diff --git a/go/modcdp/injector/ExtensionInjector.go b/go/modcdp/injector/ExtensionInjector.go
index 40e08e3..7bfc8aa 100644
--- a/go/modcdp/injector/ExtensionInjector.go
+++ b/go/modcdp/injector/ExtensionInjector.go
@@ -1,7 +1,17 @@
package injector
import (
+ "archive/zip"
+ "bytes"
+ "crypto/sha256"
+ _ "embed"
+ "encoding/base64"
+ "encoding/json"
"fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
"regexp"
"strings"
"time"
@@ -10,7 +20,6 @@ import (
)
const DefaultModCDPExtensionID = "mdedooklbnfejodmnhmkdpkaedafkehf"
-const DefaultModCDPWakePath = "/modcdp/wake.html"
const DefaultCDPSendTimeoutMS = 10_000
const DefaultExecutionContextTimeoutMS = 10_000
const DefaultServiceWorkerProbeTimeoutMS = 10_000
@@ -21,6 +30,9 @@ const DefaultTargetSessionPollIntervalMS = 20
var DefaultModCDPServiceWorkerURLSuffixes = []string{"/modcdp/service_worker.js"}
var extIDFromURL = regexp.MustCompile(`^chrome-extension://([a-z]+)/`)
+//go:embed extension.zip
+var bundledExtensionZip []byte
+
const modcdpReadyExpression = `Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)`
type SendCDP = types.SendCDP
@@ -42,9 +54,6 @@ type ExtensionInjector struct {
}
func NewExtensionInjector(options ExtensionInjectorConfig) ExtensionInjector {
- if options.InjectorWakePath == "" {
- options.InjectorWakePath = DefaultModCDPWakePath
- }
if options.InjectorCDPSendTimeoutMS == 0 {
options.InjectorCDPSendTimeoutMS = DefaultCDPSendTimeoutMS
}
@@ -85,12 +94,6 @@ func (i *ExtensionInjector) Update(config ExtensionInjectorConfig) *ExtensionInj
if config.InjectorExtensionID != "" {
i.Options.InjectorExtensionID = config.InjectorExtensionID
}
- if config.InjectorWakePath != "" {
- i.Options.InjectorWakePath = config.InjectorWakePath
- }
- if config.InjectorWakeURL != "" {
- i.Options.InjectorWakeURL = config.InjectorWakeURL
- }
if config.InjectorServiceWorkerURLIncludes != nil {
i.Options.InjectorServiceWorkerURLIncludes = append([]string{}, config.InjectorServiceWorkerURLIncludes...)
}
@@ -130,9 +133,6 @@ func (i *ExtensionInjector) Update(config ExtensionInjectorConfig) *ExtensionInj
if config.InjectorBrowserbaseBaseURL != "" {
i.Options.InjectorBrowserbaseBaseURL = config.InjectorBrowserbaseBaseURL
}
- if config.UpstreamReverseWSURL != "" {
- i.Options.UpstreamReverseWSURL = config.UpstreamReverseWSURL
- }
if config.UpstreamNativeMessagingHostName != "" {
i.Options.UpstreamNativeMessagingHostName = config.UpstreamNativeMessagingHostName
}
@@ -258,41 +258,6 @@ func (i ExtensionInjector) targetInfos() ([]map[string]any, error) {
return targets, nil
}
-func (i ExtensionInjector) configuredWakeURL() string {
- if i.Options.InjectorWakeURL != "" {
- return i.Options.InjectorWakeURL
- }
- if i.Options.InjectorExtensionID == "" {
- return ""
- }
- wakePath := i.Options.InjectorWakePath
- if wakePath == "" {
- wakePath = DefaultModCDPWakePath
- }
- if !strings.HasPrefix(wakePath, "/") {
- wakePath = "/" + wakePath
- }
- return "chrome-extension://" + i.Options.InjectorExtensionID + wakePath
-}
-
-func (i ExtensionInjector) wakeConfiguredExtension() bool {
- wakeURL := i.configuredWakeURL()
- if wakeURL == "" || i.Options.Send == nil {
- return false
- }
- _, err := i.sendWithTimeout("Target.createTarget", map[string]any{
- "url": wakeURL,
- "background": true,
- "hidden": true,
- "focus": false,
- }, "", i.Options.InjectorCDPSendTimeoutMS)
- return err == nil
-}
-
-func (i ExtensionInjector) WakeConfiguredExtension() bool {
- return i.wakeConfiguredExtension()
-}
-
func (i ExtensionInjector) probeTarget(target map[string]any, sessionTimeoutMS int, allowAttach bool) (*ExtensionInjectionResult, error) {
targetID, _ := target["targetId"].(string)
targetURL, _ := target["url"].(string)
@@ -416,3 +381,187 @@ func (i ExtensionInjector) serviceWorkerTargetMatches(target map[string]any) boo
func (i ExtensionInjector) ServiceWorkerTargetMatches(target map[string]any) bool {
return i.serviceWorkerTargetMatches(target)
}
+
+func prepareUnpackedExtension(extensionPath string) (string, string, error) {
+ if extensionPath == "" {
+ dir, err := os.MkdirTemp("", "modcdp-extension-")
+ if err != nil {
+ return "", "", err
+ }
+ reader, err := zip.NewReader(bytes.NewReader(bundledExtensionZip), int64(len(bundledExtensionZip)))
+ if err != nil {
+ _ = os.RemoveAll(dir)
+ return "", "", err
+ }
+ if err := extractZipFiles(reader.File, dir); err != nil {
+ _ = os.RemoveAll(dir)
+ return "", "", err
+ }
+ return extensionRoot(dir), dir, nil
+ }
+ if !strings.HasSuffix(extensionPath, ".zip") {
+ dir, err := os.MkdirTemp("", "modcdp-extension-")
+ if err != nil {
+ return "", "", err
+ }
+ if err := copyDir(extensionPath, dir); err != nil {
+ _ = os.RemoveAll(dir)
+ return "", "", err
+ }
+ return dir, dir, nil
+ }
+ dir, err := os.MkdirTemp("", "modcdp-extension-")
+ if err != nil {
+ return "", "", err
+ }
+ reader, err := zip.OpenReader(extensionPath)
+ if err != nil {
+ _ = os.RemoveAll(dir)
+ return "", "", err
+ }
+ defer reader.Close()
+ if err := extractZipFiles(reader.File, dir); err != nil {
+ _ = os.RemoveAll(dir)
+ return "", "", err
+ }
+ return extensionRoot(dir), dir, nil
+}
+
+func extractZipFiles(files []*zip.File, dir string) error {
+ root, err := filepath.Abs(dir)
+ if err != nil {
+ return err
+ }
+ for _, file := range files {
+ targetPath, err := safeZipTarget(root, file.Name)
+ if err != nil {
+ return err
+ }
+ if file.FileInfo().IsDir() {
+ if err := os.MkdirAll(targetPath, 0o755); err != nil {
+ return err
+ }
+ continue
+ }
+ if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
+ return err
+ }
+ src, err := file.Open()
+ if err != nil {
+ return err
+ }
+ dst, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
+ if err != nil {
+ _ = src.Close()
+ return err
+ }
+ _, copyErr := io.Copy(dst, src)
+ srcErr := src.Close()
+ dstErr := dst.Close()
+ if copyErr != nil {
+ return copyErr
+ }
+ if srcErr != nil {
+ return srcErr
+ }
+ if dstErr != nil {
+ return dstErr
+ }
+ }
+ return nil
+}
+
+func safeZipTarget(root string, name string) (string, error) {
+ cleanName := filepath.Clean(name)
+ if filepath.IsAbs(cleanName) || cleanName == "." || cleanName == ".." || strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) {
+ return "", fmt.Errorf("zip entry %q escapes extension extraction directory", name)
+ }
+ targetPath := filepath.Join(root, cleanName)
+ targetAbs, err := filepath.Abs(targetPath)
+ if err != nil {
+ return "", err
+ }
+ if targetAbs != root && !strings.HasPrefix(targetAbs, root+string(os.PathSeparator)) {
+ return "", fmt.Errorf("zip entry %q escapes extension extraction directory", name)
+ }
+ return targetAbs, nil
+}
+
+func extensionRoot(unpackedPath string) string {
+ if _, err := os.Stat(filepath.Join(unpackedPath, "manifest.json")); err == nil {
+ return unpackedPath
+ }
+ nested := filepath.Join(unpackedPath, "extension")
+ if _, err := os.Stat(filepath.Join(nested, "manifest.json")); err == nil {
+ return nested
+ }
+ return unpackedPath
+}
+
+func extensionIDFromManifestKey(extensionPath string) (string, error) {
+ manifestBytes, err := os.ReadFile(filepath.Join(extensionPath, "manifest.json"))
+ if err != nil {
+ return "", nil
+ }
+ var manifest map[string]any
+ if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
+ return "", err
+ }
+ key, _ := manifest["key"].(string)
+ if strings.TrimSpace(key) == "" {
+ return "", nil
+ }
+ keyBytes, err := base64.StdEncoding.DecodeString(key)
+ if err != nil {
+ return "", err
+ }
+ digest := sha256.Sum256(keyBytes)
+ alphabet := "abcdefghijklmnop"
+ result := strings.Builder{}
+ for _, value := range digest[:16] {
+ result.WriteByte(alphabet[value>>4])
+ result.WriteByte(alphabet[value&0x0f])
+ }
+ return result.String(), nil
+}
+
+func copyDir(src string, dst string) error {
+ return filepath.WalkDir(src, func(path string, entry fs.DirEntry, walkErr error) error {
+ if walkErr != nil {
+ return walkErr
+ }
+ relative, err := filepath.Rel(src, path)
+ if err != nil {
+ return err
+ }
+ target := filepath.Join(dst, relative)
+ if entry.IsDir() {
+ return os.MkdirAll(target, 0o755)
+ }
+ info, err := entry.Info()
+ if err != nil {
+ return err
+ }
+ sourceFile, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer sourceFile.Close()
+ targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
+ if err != nil {
+ return err
+ }
+ defer targetFile.Close()
+ _, err = io.Copy(targetFile, sourceFile)
+ return err
+ })
+}
+
+func firstNonEmptyString(values ...string) string {
+ for _, value := range values {
+ if strings.TrimSpace(value) != "" {
+ return strings.TrimSpace(value)
+ }
+ }
+ return ""
+}
diff --git a/go/modcdp/injector/ExtensionInjector_test.go b/go/modcdp/injector/ExtensionInjector_test.go
index 416c77e..3be2e6a 100644
--- a/go/modcdp/injector/ExtensionInjector_test.go
+++ b/go/modcdp/injector/ExtensionInjector_test.go
@@ -1,267 +1,13 @@
package injector_test
import (
- "context"
- "encoding/json"
- "fmt"
- modcdp "github.com/browserbase/modcdp/go/modcdp/client"
- . "github.com/browserbase/modcdp/go/modcdp/injector"
- "path/filepath"
"strings"
"testing"
- "time"
- "github.com/gobwas/ws"
- "github.com/gobwas/ws/wsutil"
+ . "github.com/browserbase/modcdp/go/modcdp/injector"
)
-type probeExtensionInjector struct {
- ExtensionInjector
-}
-
-func (i probeExtensionInjector) Inject() (*ExtensionInjectionResult, error) {
- return i.WaitForReadyServiceWorker(i.Options.InjectorServiceWorkerReadyTimeoutMS, true)
-}
-
-func TestExtensionInjectorProbesRealExtensionServiceWorkerWithSharedBaseConfig(t *testing.T) {
- extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
- if err != nil {
- t.Fatal(err)
- }
- chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
- Headless: boolPtr(true),
- Sandbox: boolPtr(false),
- ExtraArgs: []string{"--load-extension=" + extensionPath},
- }).Launch(modcdp.LaunchOptions{})
- if err != nil {
- t.Fatal(err)
- }
- defer chrome.Close()
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- conn, _, _, err := ws.Dial(ctx, chrome.CDPURL)
- if err != nil {
- t.Fatal(err)
- }
- defer conn.Close()
-
- nextID := 0
- send := func(method string, params map[string]any, sessionID string) (map[string]any, error) {
- nextID++
- message := map[string]any{"id": nextID, "method": method, "params": params}
- if sessionID != "" {
- message["sessionId"] = sessionID
- }
- body, err := json.Marshal(message)
- if err != nil {
- return nil, err
- }
- if err := wsutil.WriteClientText(conn, body); err != nil {
- return nil, err
- }
- for {
- raw, err := wsutil.ReadServerText(conn)
- if err != nil {
- return nil, err
- }
- var response map[string]any
- if err := json.Unmarshal(raw, &response); err != nil {
- return nil, err
- }
- responseID, _ := response["id"].(float64)
- if int(responseID) != nextID {
- continue
- }
- if errorObject, ok := response["error"].(map[string]any); ok {
- return nil, fmt.Errorf("%v", errorObject["message"])
- }
- result, _ := response["result"].(map[string]any)
- if result == nil {
- result = map[string]any{}
- }
- return result, nil
- }
- }
-
- injector := probeExtensionInjector{ExtensionInjector: NewExtensionInjector(ExtensionInjectorConfig{
- Send: send,
- AttachToTarget: func(targetID string) string {
- result, _ := send("Target.attachToTarget", map[string]any{"targetId": targetID, "flatten": true}, "")
- sessionID, _ := result["sessionId"].(string)
- return sessionID
- },
- InjectorExtensionID: DefaultModCDPExtensionID,
- InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
- InjectorTrustServiceWorkerTarget: true,
- })}
-
- transportConfig := injector.GetTransportConfig()
- if transportConfig["injector_extension_id"] != DefaultModCDPExtensionID {
- t.Fatalf("injector_extension_id = %v", transportConfig["injector_extension_id"])
- }
- if len(injector.GetLauncherConfig().ExtraArgs) != 0 {
- t.Fatalf("expected empty launcher config")
- }
- result, err := injector.Inject()
- if err != nil {
- t.Fatal(err)
- }
- if result == nil {
- t.Fatal("expected service worker probe result")
- }
- if result.ExtensionID != DefaultModCDPExtensionID {
- t.Fatalf("ExtensionID = %q", result.ExtensionID)
- }
- if filepath.Base(result.URL) != "service_worker.js" {
- t.Fatalf("URL = %q", result.URL)
- }
-}
-
-func TestExtensionInjectorKeepsModCDPServiceWorkerAliveThroughOffscreenKeepalive(t *testing.T) {
- extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
- if err != nil {
- t.Fatal(err)
- }
- chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
- Headless: boolPtr(true),
- Sandbox: boolPtr(false),
- ExtraArgs: []string{"--load-extension=" + extensionPath},
- }).Launch(modcdp.LaunchOptions{})
- if err != nil {
- t.Fatal(err)
- }
- defer chrome.Close()
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- conn, _, _, err := ws.Dial(ctx, chrome.CDPURL)
- if err != nil {
- t.Fatal(err)
- }
- defer conn.Close()
-
- nextID := 0
- send := func(method string, params map[string]any, sessionID string) (map[string]any, error) {
- nextID++
- message := map[string]any{"id": nextID, "method": method, "params": params}
- if sessionID != "" {
- message["sessionId"] = sessionID
- }
- body, err := json.Marshal(message)
- if err != nil {
- return nil, err
- }
- if err := wsutil.WriteClientText(conn, body); err != nil {
- return nil, err
- }
- for {
- raw, err := wsutil.ReadServerText(conn)
- if err != nil {
- return nil, err
- }
- var response map[string]any
- if err := json.Unmarshal(raw, &response); err != nil {
- return nil, err
- }
- responseID, _ := response["id"].(float64)
- if int(responseID) != nextID {
- continue
- }
- if errorObject, ok := response["error"].(map[string]any); ok {
- return nil, fmt.Errorf("%v", errorObject["message"])
- }
- result, _ := response["result"].(map[string]any)
- if result == nil {
- result = map[string]any{}
- }
- return result, nil
- }
- }
-
- injector := probeExtensionInjector{ExtensionInjector: NewExtensionInjector(ExtensionInjectorConfig{
- Send: send,
- AttachToTarget: func(targetID string) string {
- result, _ := send("Target.attachToTarget", map[string]any{"targetId": targetID, "flatten": true}, "")
- sessionID, _ := result["sessionId"].(string)
- return sessionID
- },
- InjectorExtensionID: DefaultModCDPExtensionID,
- InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
- InjectorTrustServiceWorkerTarget: true,
- })}
-
- result, err := injector.Inject()
- if err != nil {
- t.Fatal(err)
- }
- if result == nil {
- t.Fatal("expected service worker probe result")
- }
- if result.ExtensionID != DefaultModCDPExtensionID {
- t.Fatalf("ExtensionID = %q", result.ExtensionID)
- }
-
- foundOffscreen := false
- var contexts []any
- for attempt := 0; attempt < 50; attempt++ {
- contextsResult, err := send("Runtime.evaluate", map[string]any{
- "expression": "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => ({ type: context.contextType, url: context.documentUrl || context.origin || '' })))",
- "awaitPromise": true,
- "returnByValue": true,
- }, result.SessionID)
- if err != nil {
- t.Fatal(err)
- }
- evaluation, _ := contextsResult["result"].(map[string]any)
- contexts, _ = evaluation["value"].([]any)
- for _, rawContext := range contexts {
- context, _ := rawContext.(map[string]any)
- if context["type"] == "OFFSCREEN_DOCUMENT" &&
- context["url"] == "chrome-extension://"+DefaultModCDPExtensionID+"/offscreen/keepalive.html" {
- foundOffscreen = true
- }
- }
- if foundOffscreen {
- break
- }
- time.Sleep(100 * time.Millisecond)
- }
- if !foundOffscreen {
- t.Fatalf("expected offscreen keepalive context, got %#v", contexts)
- }
-
- time.Sleep(3 * time.Second)
- targetsResult, err := send("Target.getTargets", map[string]any{}, "")
- if err != nil {
- t.Fatal(err)
- }
- targets, _ := targetsResult["targetInfos"].([]any)
- foundServiceWorker := false
- for _, rawTarget := range targets {
- target, _ := rawTarget.(map[string]any)
- if target["type"] == "service_worker" &&
- target["url"] == "chrome-extension://"+DefaultModCDPExtensionID+"/modcdp/service_worker.js" {
- foundServiceWorker = true
- }
- }
- if !foundServiceWorker {
- t.Fatalf("expected service worker target, got %#v", targets)
- }
- version, err := send("Runtime.evaluate", map[string]any{
- "expression": "globalThis.ModCDP?.__ModCDPServerVersion",
- "returnByValue": true,
- }, result.SessionID)
- if err != nil {
- t.Fatal(err)
- }
- versionResult, _ := version["result"].(map[string]any)
- if versionResult["value"] != float64(2) {
- t.Fatalf("ModCDP server version = %#v", versionResult["value"])
- }
-}
-
-func TestExtensionInjectorOwnsSharedConfig(t *testing.T) {
+func TestExtensionInjectorOwnsSharedInjectorConfig(t *testing.T) {
injector := NewExtensionInjector(ExtensionInjectorConfig{
InjectorExtensionID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
@@ -279,173 +25,20 @@ func TestExtensionInjectorOwnsSharedConfig(t *testing.T) {
"type": "service_worker",
"url": "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/modcdp/service_worker.js",
}) {
- t.Fatalf("expected service worker target to match")
- }
- if _, err := injector.Inject(); err == nil {
- t.Fatalf("expected base Inject to fail")
- }
-}
-
-func TestExtensionInjectorSendWithTimeoutEnforcesCDPSendTimeout(t *testing.T) {
- chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
- Headless: boolPtr(true),
- Sandbox: boolPtr(false),
- }).Launch(modcdp.LaunchOptions{})
- if err != nil {
- t.Fatal(err)
- }
- defer chrome.Close()
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- conn, _, _, err := ws.Dial(ctx, chrome.CDPURL)
- if err != nil {
- t.Fatal(err)
- }
- defer conn.Close()
-
- nextID := 0
- send := func(method string, params map[string]any, sessionID string) (map[string]any, error) {
- nextID++
- message := map[string]any{"id": nextID, "method": method, "params": params}
- if sessionID != "" {
- message["sessionId"] = sessionID
- }
- body, err := json.Marshal(message)
- if err != nil {
- return nil, err
- }
- if err := wsutil.WriteClientText(conn, body); err != nil {
- return nil, err
- }
- for {
- raw, err := wsutil.ReadServerText(conn)
- if err != nil {
- return nil, err
- }
- var response map[string]any
- if err := json.Unmarshal(raw, &response); err != nil {
- return nil, err
- }
- responseID, _ := response["id"].(float64)
- if int(responseID) != nextID {
- continue
- }
- if errorObject, ok := response["error"].(map[string]any); ok {
- return nil, fmt.Errorf("%v", errorObject["message"])
- }
- result, _ := response["result"].(map[string]any)
- if result == nil {
- result = map[string]any{}
- }
- return result, nil
- }
- }
-
- created, err := send("Target.createTarget", map[string]any{"url": "about:blank#modcdp-timeout"}, "")
- if err != nil {
- t.Fatal(err)
+ t.Fatal("expected service worker target to match")
}
- targetID, _ := created["targetId"].(string)
- attached, err := send("Target.attachToTarget", map[string]any{"targetId": targetID, "flatten": true}, "")
- if err != nil {
- t.Fatal(err)
- }
- sessionID, _ := attached["sessionId"].(string)
- if _, err := send("Runtime.enable", map[string]any{}, sessionID); err != nil {
- t.Fatal(err)
- }
- injector := NewExtensionInjector(ExtensionInjectorConfig{
- Send: send,
- })
-
- if _, err := injector.SendWithTimeout("Runtime.evaluate", map[string]any{
- "expression": "new Promise(() => {})",
- "awaitPromise": true,
- }, sessionID, 5); err == nil || !strings.Contains(err.Error(), "Runtime.evaluate timed out after 5ms") {
- t.Fatalf("SendWithTimeout error = %v", err)
+ if injector.ServiceWorkerTargetMatches(map[string]any{
+ "targetId": "target-1",
+ "type": "service_worker",
+ "url": "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/background.js",
+ }) {
+ t.Fatal("expected background target not to match")
}
}
-func TestExtensionInjectorWakesConfiguredExtensionWithHiddenBackgroundTarget(t *testing.T) {
- extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
- if err != nil {
- t.Fatal(err)
- }
- chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
- Headless: boolPtr(true),
- Sandbox: boolPtr(false),
- ExtraArgs: []string{"--load-extension=" + extensionPath},
- }).Launch(modcdp.LaunchOptions{})
- if err != nil {
- t.Fatal(err)
- }
- defer chrome.Close()
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- conn, _, _, err := ws.Dial(ctx, chrome.CDPURL)
- if err != nil {
- t.Fatal(err)
- }
- defer conn.Close()
-
- nextID := 0
- send := func(method string, params map[string]any, sessionID string) (map[string]any, error) {
- nextID++
- message := map[string]any{"id": nextID, "method": method, "params": params}
- if sessionID != "" {
- message["sessionId"] = sessionID
- }
- body, err := json.Marshal(message)
- if err != nil {
- return nil, err
- }
- if err := wsutil.WriteClientText(conn, body); err != nil {
- return nil, err
- }
- for {
- raw, err := wsutil.ReadServerText(conn)
- if err != nil {
- return nil, err
- }
- var response map[string]any
- if err := json.Unmarshal(raw, &response); err != nil {
- return nil, err
- }
- responseID, _ := response["id"].(float64)
- if int(responseID) != nextID {
- continue
- }
- if errorObject, ok := response["error"].(map[string]any); ok {
- return nil, fmt.Errorf("%v", errorObject["message"])
- }
- result, _ := response["result"].(map[string]any)
- if result == nil {
- result = map[string]any{}
- }
- return result, nil
- }
- }
-
- injector := NewExtensionInjector(ExtensionInjectorConfig{
- InjectorExtensionID: DefaultModCDPExtensionID,
- Send: send,
- })
-
- if !injector.WakeConfiguredExtension() {
- t.Fatalf("expected wake to succeed")
- }
- targetsResult, err := send("Target.getTargets", map[string]any{}, "")
- if err != nil {
- t.Fatal(err)
- }
- targets, _ := targetsResult["targetInfos"].([]any)
- for _, rawTarget := range targets {
- target, _ := rawTarget.(map[string]any)
- if target["url"] == "chrome-extension://"+DefaultModCDPExtensionID+"/modcdp/wake.html" {
- return
- }
+func TestExtensionInjectorBaseInjectReportsTheClassName(t *testing.T) {
+ injector := NewExtensionInjector(ExtensionInjectorConfig{})
+ if _, err := injector.Inject(); err == nil || !strings.Contains(err.Error(), "ExtensionInjector.Inject is not implemented") {
+ t.Fatalf("Inject error = %v", err)
}
- t.Fatalf("expected wake target, got %#v", targets)
}
diff --git a/go/modcdp/injector/ExtensionsLoadUnpackedInjector.go b/go/modcdp/injector/ExtensionsLoadUnpackedInjector.go
index 2c80efa..d13def9 100644
--- a/go/modcdp/injector/ExtensionsLoadUnpackedInjector.go
+++ b/go/modcdp/injector/ExtensionsLoadUnpackedInjector.go
@@ -22,7 +22,7 @@ func (i *ExtensionsLoadUnpackedInjector) Prepare() error {
if i.UnpackedExtensionPath != "" {
return nil
}
- unpackedPath, cleanupPath, err := prepareUnpackedExtension(extensionPath, true)
+ unpackedPath, cleanupPath, err := prepareUnpackedExtension(extensionPath)
if err != nil {
return err
}
@@ -51,7 +51,6 @@ func (i *ExtensionsLoadUnpackedInjector) Inject() (*ExtensionInjectionResult, er
return nil, fmt.Errorf("Extensions.loadUnpacked returned no extension id")
}
i.Options.InjectorExtensionID = extensionID
- i.wakeConfiguredExtension()
swURLPrefix := "chrome-extension://" + extensionID + "/"
deadline := time.Now().Add(time.Duration(i.Options.InjectorServiceWorkerReadyTimeoutMS) * time.Millisecond)
for time.Now().Before(deadline) {
diff --git a/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go b/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go
index 3c89bad..9251461 100644
--- a/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go
+++ b/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go
@@ -1,103 +1,28 @@
package injector_test
import (
- "context"
- "encoding/json"
- "fmt"
- modcdp "github.com/browserbase/modcdp/go/modcdp/client"
- . "github.com/browserbase/modcdp/go/modcdp/injector"
+ "os"
"path/filepath"
"strings"
"testing"
- "time"
- "github.com/gobwas/ws"
- "github.com/gobwas/ws/wsutil"
+ . "github.com/browserbase/modcdp/go/modcdp/injector"
)
-func TestExtensionsLoadUnpackedInjectorExercisesRealCDPLoadUnpackedPath(t *testing.T) {
- extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
- if err != nil {
- t.Fatal(err)
- }
- chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
- Headless: boolPtr(true),
- Sandbox: boolPtr(false),
- }).Launch(modcdp.LaunchOptions{})
- if err != nil {
- t.Fatal(err)
- }
- defer chrome.Close()
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- conn, _, _, err := ws.Dial(ctx, chrome.CDPURL)
- if err != nil {
- t.Fatal(err)
- }
- defer conn.Close()
-
- nextID := 0
- send := func(method string, params map[string]any, sessionID string) (map[string]any, error) {
- nextID++
- message := map[string]any{"id": nextID, "method": method, "params": params}
- if sessionID != "" {
- message["sessionId"] = sessionID
- }
- body, err := json.Marshal(message)
- if err != nil {
- return nil, err
- }
- if err := wsutil.WriteClientText(conn, body); err != nil {
- return nil, err
- }
- for {
- raw, err := wsutil.ReadServerText(conn)
- if err != nil {
- return nil, err
- }
- var response map[string]any
- if err := json.Unmarshal(raw, &response); err != nil {
- return nil, err
- }
- responseID, _ := response["id"].(float64)
- if int(responseID) != nextID {
- continue
- }
- if errorObject, ok := response["error"].(map[string]any); ok {
- return nil, fmt.Errorf("%v", errorObject["message"])
- }
- result, _ := response["result"].(map[string]any)
- if result == nil {
- result = map[string]any{}
- }
- return result, nil
- }
- }
-
- injector := NewExtensionsLoadUnpackedInjector(ExtensionInjectorConfig{
- Send: send,
- InjectorExtensionPath: extensionPath,
- })
+func TestExtensionsLoadUnpackedInjectorPreparesDefaultPackagedExtensionZip(t *testing.T) {
+ injector := NewExtensionsLoadUnpackedInjector(ExtensionInjectorConfig{})
if err := injector.Prepare(); err != nil {
t.Fatal(err)
}
defer injector.Close()
- result, err := injector.Inject()
- if err != nil {
- t.Fatal(err)
- }
- if result != nil {
- t.Fatalf("result = %#v", result)
+ if injector.UnpackedExtensionPath == "" {
+ t.Fatal("expected UnpackedExtensionPath")
}
- if injector.LastError == nil {
- t.Fatal("expected LastError from real Extensions.loadUnpacked attempt")
+ if !strings.Contains(injector.UnpackedExtensionPath, "modcdp-extension-") {
+ t.Fatalf("UnpackedExtensionPath = %q", injector.UnpackedExtensionPath)
}
- message := injector.LastError.Error()
- if !strings.Contains(message, "Method not available") &&
- !strings.Contains(message, "Method not found") &&
- !strings.Contains(message, "wasn't found") {
- t.Fatalf("LastError = %v", injector.LastError)
+ if _, err := os.Stat(filepath.Join(injector.UnpackedExtensionPath, "manifest.json")); err != nil {
+ t.Fatalf("expected unpacked manifest: %v", err)
}
}
diff --git a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector.go b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector.go
index d6f14a5..927bdaf 100644
--- a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector.go
+++ b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector.go
@@ -1,23 +1,9 @@
package injector
import (
- "archive/zip"
- "bytes"
- "crypto/sha256"
- _ "embed"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io"
- "io/fs"
"os"
- "path/filepath"
- "strings"
)
-//go:embed extension.zip
-var bundledExtensionZip []byte
-
type LocalBrowserLaunchExtensionInjector struct {
ExtensionInjector
UnpackedExtensionPath string
@@ -34,7 +20,7 @@ func (i *LocalBrowserLaunchExtensionInjector) Prepare() error {
if i.UnpackedExtensionPath != "" {
return nil
}
- unpackedPath, cleanupPath, err := prepareUnpackedExtension(extensionPath, true)
+ unpackedPath, cleanupPath, err := prepareUnpackedExtension(extensionPath)
if err != nil {
return err
}
@@ -52,8 +38,7 @@ func (i *LocalBrowserLaunchExtensionInjector) GetLauncherConfig() LaunchOptions
}
func (i *LocalBrowserLaunchExtensionInjector) Inject() (*ExtensionInjectionResult, error) {
- timeoutMS := i.Options.InjectorServiceWorkerProbeTimeoutMS
- discovered, err := i.waitForReadyServiceWorker(timeoutMS, i.Options.InjectorTrustServiceWorkerTarget)
+ discovered, err := i.discoverReadyServiceWorker(i.Options.InjectorTrustServiceWorkerTarget)
if err != nil || discovered == nil {
return discovered, err
}
@@ -87,190 +72,3 @@ func (i *LocalBrowserLaunchExtensionInjector) resolveExtensionID() (string, erro
}
return i.ExtensionID, nil
}
-
-func prepareUnpackedExtension(extensionPath string, copyDirectory bool) (string, string, error) {
- if extensionPath == "" {
- dir, err := os.MkdirTemp("", "modcdp-extension-")
- if err != nil {
- return "", "", err
- }
- reader, err := zip.NewReader(bytes.NewReader(bundledExtensionZip), int64(len(bundledExtensionZip)))
- if err != nil {
- _ = os.RemoveAll(dir)
- return "", "", err
- }
- if err := extractZipFiles(reader.File, dir); err != nil {
- _ = os.RemoveAll(dir)
- return "", "", err
- }
- return extensionRoot(dir), dir, nil
- }
- if !strings.HasSuffix(extensionPath, ".zip") {
- if !copyDirectory {
- return extensionPath, "", nil
- }
- dir, err := os.MkdirTemp("", "modcdp-extension-")
- if err != nil {
- return "", "", err
- }
- if err := copyDir(extensionPath, dir); err != nil {
- _ = os.RemoveAll(dir)
- return "", "", err
- }
- return dir, dir, nil
- }
- dir, err := os.MkdirTemp("", "modcdp-extension-")
- if err != nil {
- return "", "", err
- }
- reader, err := zip.OpenReader(extensionPath)
- if err != nil {
- _ = os.RemoveAll(dir)
- return "", "", err
- }
- defer reader.Close()
- if err := extractZipFiles(reader.File, dir); err != nil {
- _ = os.RemoveAll(dir)
- return "", "", err
- }
- return extensionRoot(dir), dir, nil
-}
-
-func extractZipFiles(files []*zip.File, dir string) error {
- root, err := filepath.Abs(dir)
- if err != nil {
- return err
- }
- for _, file := range files {
- targetPath, err := safeZipTarget(root, file.Name)
- if err != nil {
- return err
- }
- if file.FileInfo().IsDir() {
- if err := os.MkdirAll(targetPath, 0o755); err != nil {
- return err
- }
- continue
- }
- if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
- return err
- }
- src, err := file.Open()
- if err != nil {
- return err
- }
- dst, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
- if err != nil {
- _ = src.Close()
- return err
- }
- _, copyErr := io.Copy(dst, src)
- srcErr := src.Close()
- dstErr := dst.Close()
- if copyErr != nil {
- return copyErr
- }
- if srcErr != nil {
- return srcErr
- }
- if dstErr != nil {
- return dstErr
- }
- }
- return nil
-}
-
-func safeZipTarget(root string, name string) (string, error) {
- cleanName := filepath.Clean(name)
- if filepath.IsAbs(cleanName) || cleanName == "." || cleanName == ".." || strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) {
- return "", fmt.Errorf("zip entry %q escapes extension extraction directory", name)
- }
- targetPath := filepath.Join(root, cleanName)
- targetAbs, err := filepath.Abs(targetPath)
- if err != nil {
- return "", err
- }
- if targetAbs != root && !strings.HasPrefix(targetAbs, root+string(os.PathSeparator)) {
- return "", fmt.Errorf("zip entry %q escapes extension extraction directory", name)
- }
- return targetAbs, nil
-}
-
-func extensionRoot(unpackedPath string) string {
- if _, err := os.Stat(filepath.Join(unpackedPath, "manifest.json")); err == nil {
- return unpackedPath
- }
- nested := filepath.Join(unpackedPath, "extension")
- if _, err := os.Stat(filepath.Join(nested, "manifest.json")); err == nil {
- return nested
- }
- return unpackedPath
-}
-
-func extensionIDFromManifestKey(extensionPath string) (string, error) {
- manifestBytes, err := os.ReadFile(filepath.Join(extensionPath, "manifest.json"))
- if err != nil {
- return "", nil
- }
- var manifest map[string]any
- if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
- return "", err
- }
- key, _ := manifest["key"].(string)
- if strings.TrimSpace(key) == "" {
- return "", nil
- }
- keyBytes, err := base64.StdEncoding.DecodeString(key)
- if err != nil {
- return "", err
- }
- digest := sha256.Sum256(keyBytes)
- alphabet := "abcdefghijklmnop"
- result := strings.Builder{}
- for _, value := range digest[:16] {
- result.WriteByte(alphabet[value>>4])
- result.WriteByte(alphabet[value&0x0f])
- }
- return result.String(), nil
-}
-
-func copyDir(src string, dst string) error {
- return filepath.WalkDir(src, func(path string, entry fs.DirEntry, walkErr error) error {
- if walkErr != nil {
- return walkErr
- }
- relative, err := filepath.Rel(src, path)
- if err != nil {
- return err
- }
- target := filepath.Join(dst, relative)
- if entry.IsDir() {
- return os.MkdirAll(target, 0o755)
- }
- info, err := entry.Info()
- if err != nil {
- return err
- }
- sourceFile, err := os.Open(path)
- if err != nil {
- return err
- }
- defer sourceFile.Close()
- targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
- if err != nil {
- return err
- }
- defer targetFile.Close()
- _, err = io.Copy(targetFile, sourceFile)
- return err
- })
-}
-
-func firstNonEmptyString(values ...string) string {
- for _, value := range values {
- if strings.TrimSpace(value) != "" {
- return strings.TrimSpace(value)
- }
- }
- return ""
-}
diff --git a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go
index 60ea660..3d19046 100644
--- a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go
+++ b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go
@@ -2,13 +2,12 @@ package injector_test
import (
"archive/zip"
- modcdp "github.com/browserbase/modcdp/go/modcdp/client"
. "github.com/browserbase/modcdp/go/modcdp/injector"
"os"
"path/filepath"
- "runtime"
"strings"
"testing"
+ "time"
)
func TestLocalBrowserLaunchExtensionInjectorRejectsZipEntriesOutsideExtractionDir(t *testing.T) {
@@ -42,73 +41,96 @@ func TestLocalBrowserLaunchExtensionInjectorRejectsZipEntriesOutsideExtractionDi
}
}
-func TestLocalBrowserLaunchExtensionInjectorLoadsRealExtensionDuringLocalLaunch(t *testing.T) {
+func TestLocalBrowserLaunchExtensionInjectorPreparesUnpackedExtensionDirectoryForLoadExtension(t *testing.T) {
extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
if err != nil {
t.Fatal(err)
}
- headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == ""
- sandbox := runtime.GOOS != "linux"
- cdp := modcdp.New(modcdp.Options{
- Launcher: modcdp.LauncherConfig{LauncherMode: "local",
- LauncherOptions: modcdp.LaunchOptions{
- Headless: boolPtr(headless),
- Sandbox: boolPtr(sandbox),
- },
- },
- Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws"},
- Injector: modcdp.InjectorConfig{
- InjectorMode: "inject",
- InjectorExtensionPath: extensionPath,
- InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
- InjectorTrustServiceWorkerTarget: true,
- InjectorServiceWorkerProbeTimeoutMS: 30_000,
- },
- Client: modcdp.ClientConfig{
- ClientCDPSendTimeoutMS: 30_000,
- },
- })
- defer cdp.Close()
-
- if err := cdp.Connect(); err != nil {
+ injector := NewLocalBrowserLaunchExtensionInjector(ExtensionInjectorConfig{InjectorExtensionPath: extensionPath})
+ if err := injector.Prepare(); err != nil {
t.Fatal(err)
}
- if cdp.ConnectTiming["injector_source"] != "local_launch" {
- t.Fatalf("injector_source = %v", cdp.ConnectTiming["injector_source"])
+ defer injector.Close()
+
+ if injector.UnpackedExtensionPath == "" {
+ t.Fatal("expected UnpackedExtensionPath")
}
- if cdp.ExtensionID != DefaultModCDPExtensionID {
- t.Fatalf("ExtensionID = %q", cdp.ExtensionID)
+ if injector.UnpackedExtensionPath == extensionPath {
+ t.Fatalf("UnpackedExtensionPath = %q", injector.UnpackedExtensionPath)
}
- if cdp.ExtSessionID == "" {
- t.Fatal("expected ExtSessionID")
+ if _, err := os.Stat(filepath.Join(injector.UnpackedExtensionPath, "manifest.json")); err != nil {
+ t.Fatalf("expected unpacked manifest: %v", err)
}
- result, err := cdp.Mod.Evaluate(map[string]any{
- "expression": "chrome.runtime.getURL('modcdp/service_worker.js')",
- })
- if err != nil {
+ launcherConfig := injector.GetLauncherConfig()
+ if len(launcherConfig.ExtraArgs) != 1 || launcherConfig.ExtraArgs[0] != "--load-extension="+injector.UnpackedExtensionPath {
+ t.Fatalf("ExtraArgs = %#v", launcherConfig.ExtraArgs)
+ }
+ if injector.Options.InjectorExtensionID != DefaultModCDPExtensionID {
+ t.Fatalf("InjectorExtensionID = %q", injector.Options.InjectorExtensionID)
+ }
+}
+
+func TestLocalBrowserLaunchExtensionInjectorPreparesDefaultExtensionZipForLoadExtension(t *testing.T) {
+ injector := NewLocalBrowserLaunchExtensionInjector(ExtensionInjectorConfig{})
+ if err := injector.Prepare(); err != nil {
t.Fatal(err)
}
- if result != "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js" {
- t.Fatalf("Mod.evaluate = %#v", result)
+ defer injector.Close()
+
+ if injector.UnpackedExtensionPath == "" {
+ t.Fatal("expected UnpackedExtensionPath")
+ }
+ if !strings.Contains(injector.UnpackedExtensionPath, "modcdp-extension-") {
+ t.Fatalf("UnpackedExtensionPath = %q", injector.UnpackedExtensionPath)
+ }
+ if _, err := os.Stat(filepath.Join(injector.UnpackedExtensionPath, "manifest.json")); err != nil {
+ t.Fatalf("expected unpacked manifest: %v", err)
+ }
+ launcherConfig := injector.GetLauncherConfig()
+ if len(launcherConfig.ExtraArgs) != 1 || launcherConfig.ExtraArgs[0] != "--load-extension="+injector.UnpackedExtensionPath {
+ t.Fatalf("ExtraArgs = %#v", launcherConfig.ExtraArgs)
+ }
+ if injector.Options.InjectorExtensionID != DefaultModCDPExtensionID {
+ t.Fatalf("InjectorExtensionID = %q", injector.Options.InjectorExtensionID)
}
}
-func TestLocalBrowserLaunchExtensionInjectorPreparesLauncherConfig(t *testing.T) {
+func TestLocalBrowserLaunchExtensionInjectorReturnsImmediatelyWhenLaunchedExtensionTargetIsAbsent(t *testing.T) {
extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
if err != nil {
t.Fatal(err)
}
- injector := NewLocalBrowserLaunchExtensionInjector(ExtensionInjectorConfig{InjectorExtensionPath: extensionPath})
+ methods := []string{}
+ injector := NewLocalBrowserLaunchExtensionInjector(ExtensionInjectorConfig{
+ InjectorExtensionPath: extensionPath,
+ InjectorTrustServiceWorkerTarget: true,
+ Send: func(method string, params map[string]any, sessionID string) (map[string]any, error) {
+ methods = append(methods, method)
+ if method == "Target.getTargets" {
+ return map[string]any{"targetInfos": []any{}}, nil
+ }
+ t.Fatalf("unexpected %s", method)
+ return nil, nil
+ },
+ })
if err := injector.Prepare(); err != nil {
t.Fatal(err)
}
defer injector.Close()
- launchConfig := injector.GetLauncherConfig()
- if len(launchConfig.ExtraArgs) != 1 || !strings.HasPrefix(launchConfig.ExtraArgs[0], "--load-extension=") {
- t.Fatalf("ExtraArgs = %v", launchConfig.ExtraArgs)
+ startedAt := time.Now()
+ result, err := injector.Inject()
+ if err != nil {
+ t.Fatal(err)
+ }
+ elapsed := time.Since(startedAt)
+ if result != nil {
+ t.Fatalf("result = %#v", result)
+ }
+ if strings.Join(methods, ",") != "Target.getTargets" {
+ t.Fatalf("methods = %#v", methods)
}
- if injector.ExtensionID != DefaultModCDPExtensionID {
- t.Fatalf("ExtensionID = %q", injector.ExtensionID)
+ if elapsed >= 200*time.Millisecond {
+ t.Fatalf("Inject took %s", elapsed)
}
}
diff --git a/go/modcdp/injector/extension.zip b/go/modcdp/injector/extension.zip
index 268e970..2f87949 100644
Binary files a/go/modcdp/injector/extension.zip and b/go/modcdp/injector/extension.zip differ
diff --git a/go/modcdp/launcher/LocalBrowserLauncher.go b/go/modcdp/launcher/LocalBrowserLauncher.go
index dc20755..617e5b5 100644
--- a/go/modcdp/launcher/LocalBrowserLauncher.go
+++ b/go/modcdp/launcher/LocalBrowserLauncher.go
@@ -52,12 +52,6 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser,
usePipe := options.RemoteDebugging == "pipe"
useLoopbackCDP := !usePipe || options.Port != 0 || (options.LoopbackCDP != nil && *options.LoopbackCDP)
port := options.Port
- if useLoopbackCDP && port == 0 {
- port, err = l.FreePort()
- if err != nil {
- return nil, err
- }
- }
profileDir := options.UserDataDir
ownsProfileDir := false
if profileDir == "" {
@@ -102,7 +96,11 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser,
if headless {
args = append(args, "--headless=new")
}
- if options.Sandbox == nil || !*options.Sandbox {
+ sandbox := runtime.GOOS != "linux"
+ if options.Sandbox != nil {
+ sandbox = *options.Sandbox
+ }
+ if !sandbox {
args = append(args, "--no-sandbox")
}
args = append(args, options.Args...)
@@ -149,6 +147,13 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser,
}
return nil, err
}
+ processDone := make(chan struct{})
+ var processState *os.ProcessState
+ var processWaitErr error
+ go func() {
+ processState, processWaitErr = cmd.Process.Wait()
+ close(processDone)
+ }()
close := func() {
if pipeRead != nil {
_ = pipeRead.Close()
@@ -157,39 +162,60 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser,
_ = pipeWrite.Close()
}
if cmd.Process != nil {
- if runtime.GOOS != "windows" {
- _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)
- } else {
- _ = cmd.Process.Kill()
- }
- done := make(chan struct{})
- go func() {
- _, _ = cmd.Process.Wait()
- close(done)
- }()
select {
- case <-done:
- case <-time.After(2 * time.Second):
+ case <-processDone:
+ default:
if runtime.GOOS != "windows" {
- _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
+ _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)
} else {
_ = cmd.Process.Kill()
}
- <-done
+ select {
+ case <-processDone:
+ case <-time.After(2 * time.Second):
+ if runtime.GOOS != "windows" {
+ _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
+ } else {
+ _ = cmd.Process.Kill()
+ }
+ <-processDone
+ }
}
}
if cleanupProfileDir {
removeProfileDir(profileDir)
}
}
+ processExitedError := func() error {
+ select {
+ case <-processDone:
+ if processWaitErr != nil {
+ return fmt.Errorf("Chrome exited before CDP became ready: %w", processWaitErr)
+ }
+ if processState != nil {
+ return fmt.Errorf("Chrome exited before CDP became ready (exit=%d)", processState.ExitCode())
+ }
+ return fmt.Errorf("Chrome exited before CDP became ready")
+ default:
+ return nil
+ }
+ }
if usePipe {
+ if err := processExitedError(); err != nil {
+ close()
+ return nil, err
+ }
if err := waitForPipeReady(pipeRead, pipeWrite, time.Duration(chromeReadyTimeoutMS)*time.Millisecond); err != nil {
close()
return nil, err
}
loopbackCDPURL := ""
if useLoopbackCDP {
- loopbackCDPURL, err = waitForCdpWebSocketURL(fmt.Sprintf("http://127.0.0.1:%d", port), time.Duration(chromeReadyTimeoutMS)*time.Millisecond, time.Duration(chromeReadyPollIntervalMS)*time.Millisecond)
+ if port == 0 {
+ loopbackCDPURL, _, err = waitForBrowserSelectedCdpWebSocketURL(profileDir, time.Duration(chromeReadyTimeoutMS)*time.Millisecond, time.Duration(chromeReadyPollIntervalMS)*time.Millisecond)
+ } else {
+ loopbackCDPURL, err = waitForCdpWebSocketURL(fmt.Sprintf("http://127.0.0.1:%d", port), time.Duration(chromeReadyTimeoutMS)*time.Millisecond, time.Duration(chromeReadyPollIntervalMS)*time.Millisecond)
+ }
if err != nil {
close()
return nil, err
@@ -206,10 +232,28 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser,
l.Launched = launched
return launched, nil
}
- cdpURL := fmt.Sprintf("http://127.0.0.1:%d", port)
deadline := time.Now().Add(time.Duration(chromeReadyTimeoutMS) * time.Millisecond)
client := &http.Client{Timeout: 2 * time.Second}
for time.Now().Before(deadline) {
+ if err := processExitedError(); err != nil {
+ close()
+ return nil, err
+ }
+ cdpURL := ""
+ if port == 0 {
+ activePort, ready, err := readDevToolsActivePort(profileDir)
+ if err != nil {
+ close()
+ return nil, err
+ }
+ if !ready {
+ time.Sleep(time.Duration(chromeReadyPollIntervalMS) * time.Millisecond)
+ continue
+ }
+ cdpURL = fmt.Sprintf("http://127.0.0.1:%d", activePort)
+ } else {
+ cdpURL = fmt.Sprintf("http://127.0.0.1:%d", port)
+ }
resp, err := client.Get(cdpURL + "/json/version")
if err == nil {
var version map[string]any
@@ -229,7 +273,7 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser,
time.Sleep(time.Duration(chromeReadyPollIntervalMS) * time.Millisecond)
}
close()
- return nil, fmt.Errorf("Chrome at %s did not become ready within %dms", cdpURL, chromeReadyTimeoutMS)
+ return nil, fmt.Errorf("Chrome did not become ready within %dms", chromeReadyTimeoutMS)
}
func waitForPipeReady(pipeRead *os.File, pipeWrite *os.File, timeout time.Duration) error {
@@ -279,6 +323,49 @@ func waitForCdpWebSocketURL(cdpURL string, timeout time.Duration, pollInterval t
return "", fmt.Errorf("Chrome at %s did not expose a WebSocket CDP URL within %s", cdpURL, timeout)
}
+func readDevToolsActivePort(profileDir string) (int, bool, error) {
+ body, err := os.ReadFile(filepath.Join(profileDir, "DevToolsActivePort"))
+ if err != nil {
+ if os.IsNotExist(err) {
+ return 0, false, nil
+ }
+ return 0, false, err
+ }
+ lines := strings.Split(strings.TrimSpace(string(body)), "\n")
+ if len(lines) < 2 || strings.TrimSpace(lines[0]) == "" || strings.TrimSpace(lines[1]) == "" {
+ return 0, false, nil
+ }
+ port, err := strconv.Atoi(strings.TrimSpace(lines[0]))
+ if err != nil || port <= 0 {
+ return 0, false, fmt.Errorf("invalid DevToolsActivePort port: %s", strings.TrimSpace(lines[0]))
+ }
+ return port, true, nil
+}
+
+func waitForBrowserSelectedCdpWebSocketURL(profileDir string, timeout time.Duration, pollInterval time.Duration) (string, int, error) {
+ deadline := time.Now().Add(timeout)
+ var lastErr error
+ for time.Now().Before(deadline) {
+ port, ready, err := readDevToolsActivePort(profileDir)
+ if err != nil {
+ return "", 0, err
+ }
+ if ready {
+ cdpURL := fmt.Sprintf("http://127.0.0.1:%d", port)
+ loopbackCDPURL, err := websocketURLFor(cdpURL)
+ if err == nil && loopbackCDPURL != "" {
+ return loopbackCDPURL, port, nil
+ }
+ lastErr = err
+ }
+ time.Sleep(pollInterval)
+ }
+ if lastErr != nil {
+ return "", 0, fmt.Errorf("Chrome did not expose DevToolsActivePort from %s within %s: %w", profileDir, timeout, lastErr)
+ }
+ return "", 0, fmt.Errorf("Chrome did not expose DevToolsActivePort from %s within %s", profileDir, timeout)
+}
+
func removeProfileDir(profileDir string) {
for attempt := 0; attempt < 5; attempt++ {
if err := os.RemoveAll(profileDir); err == nil {
@@ -359,6 +446,7 @@ func candidatePaths() []string {
var canary []string
var stock []string
+ var chromium []string
switch runtime.GOOS {
case "darwin":
canary = []string{"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"}
@@ -367,12 +455,14 @@ func candidatePaths() []string {
canary = append([]string{filepath.Join(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe")}, joinAll(programFiles, "Google", "Chrome SxS", "Application", "chrome.exe")...)
stock = append(joinAll(programFiles, "Google", "Chrome", "Application", "chrome.exe"), filepath.Join(localAppData, "Google", "Chrome", "Application", "chrome.exe"))
default:
+ chromium = []string{"/usr/bin/chromium", "/usr/bin/chromium-browser"}
canary = []string{"/usr/bin/google-chrome-canary", "/usr/bin/google-chrome-unstable", "/opt/google/chrome-unstable/chrome"}
stock = []string{"/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/opt/google/chrome/chrome"}
}
- result := append([]string{}, chromeForTestingCandidates()...)
+ result := append([]string{}, chromium...)
result = append(result, canary...)
+ result = append(result, chromeForTestingCandidates()...)
result = append(result, stock...)
return result
}
diff --git a/go/modcdp/launcher/LocalBrowserLauncher_test.go b/go/modcdp/launcher/LocalBrowserLauncher_test.go
index 404f8b6..92f463d 100644
--- a/go/modcdp/launcher/LocalBrowserLauncher_test.go
+++ b/go/modcdp/launcher/LocalBrowserLauncher_test.go
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"os"
+ "runtime"
"strconv"
"strings"
"testing"
@@ -23,9 +24,8 @@ func TestLocalBrowserLauncherClassHelpersMatchLocalLauncherSurface(t *testing.T)
}
}
-func TestLocalBrowserLauncherLaunchesRealBrowserAndSpeaksCDP(t *testing.T) {
+func TestLocalBrowserLauncherLaunchesRealBrowserOverChosenCDPPortAndHonorsLaunchOptions(t *testing.T) {
headless := true
- sandbox := false
profileDir := t.TempDir()
port, err := freePort()
if err != nil {
@@ -33,7 +33,6 @@ func TestLocalBrowserLauncherLaunchesRealBrowserAndSpeaksCDP(t *testing.T) {
}
launcher := NewLocalBrowserLauncher(LaunchOptions{
Headless: &headless,
- Sandbox: &sandbox,
ChromeReadyTimeoutMS: 45_000,
ChromeReadyPollIntervalMS: 50,
})
@@ -103,14 +102,42 @@ func TestLocalBrowserLauncherLaunchesRealBrowserAndSpeaksCDP(t *testing.T) {
if response.Result.ProtocolVersion == "" {
t.Fatal("expected protocolVersion")
}
+ if err := wsutil.WriteClientText(conn, []byte(`{"id":2,"method":"SystemInfo.getInfo","params":{}}`)); err != nil {
+ t.Fatal(err)
+ }
+ systemInfoBody, err := wsutil.ReadServerText(conn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var systemInfoResponse struct {
+ ID int `json:"id"`
+ Result struct {
+ CommandLine string `json:"commandLine"`
+ } `json:"result"`
+ }
+ if err := json.Unmarshal(systemInfoBody, &systemInfoResponse); err != nil {
+ t.Fatal(err)
+ }
+ if systemInfoResponse.ID != 2 {
+ t.Fatalf("unexpected SystemInfo.getInfo response id %d", systemInfoResponse.ID)
+ }
+ command := systemInfoResponse.Result.CommandLine
+ if !strings.Contains(command, "--window-size=900,700") {
+ t.Fatalf("expected browser command to include --window-size=900,700: %s", command)
+ }
+ if runtime.GOOS == "linux" {
+ if !strings.Contains(command, "--no-sandbox") {
+ t.Fatalf("expected Linux browser command to include --no-sandbox: %s", command)
+ }
+ } else if strings.Contains(command, "--no-sandbox") {
+ t.Fatalf("expected browser command not to include --no-sandbox: %s", command)
+ }
}
func TestLocalBrowserLauncherLaunchesRealBrowserOverRemoteDebuggingPipe(t *testing.T) {
headless := true
- sandbox := false
launcher := NewLocalBrowserLauncher(LaunchOptions{
Headless: &headless,
- Sandbox: &sandbox,
ChromeReadyTimeoutMS: 45_000,
})
chrome, err := launcher.Launch(LaunchOptions{RemoteDebugging: "pipe"})
@@ -159,11 +186,9 @@ func TestLocalBrowserLauncherLaunchesRealBrowserOverRemoteDebuggingPipe(t *testi
func TestLocalBrowserLauncherLaunchesPipeBrowserWithAuxiliaryLoopbackOnlyWhenRequested(t *testing.T) {
headless := true
- sandbox := false
loopbackCDP := true
chrome, err := NewLocalBrowserLauncher(LaunchOptions{
Headless: &headless,
- Sandbox: &sandbox,
ChromeReadyTimeoutMS: 45_000,
}).Launch(LaunchOptions{RemoteDebugging: "pipe", LoopbackCDP: &loopbackCDP})
if err != nil {
@@ -201,7 +226,6 @@ func TestLocalBrowserLauncherLaunchesPipeBrowserWithAuxiliaryLoopbackOnlyWhenReq
func TestLocalBrowserLauncherCleansExplicitUserDataDirWhenRequested(t *testing.T) {
headless := true
- sandbox := false
cleanupUserDataDir := true
profileDir, err := os.MkdirTemp("", "modcdp-go-local-profile-")
if err != nil {
@@ -209,7 +233,6 @@ func TestLocalBrowserLauncherCleansExplicitUserDataDirWhenRequested(t *testing.T
}
chrome, err := NewLocalBrowserLauncher(LaunchOptions{
Headless: &headless,
- Sandbox: &sandbox,
ChromeReadyTimeoutMS: 45_000,
}).Launch(LaunchOptions{
UserDataDir: profileDir,
diff --git a/go/modcdp/launcher/RemoteBrowserLauncher_test.go b/go/modcdp/launcher/RemoteBrowserLauncher_test.go
index 2268475..20ad381 100644
--- a/go/modcdp/launcher/RemoteBrowserLauncher_test.go
+++ b/go/modcdp/launcher/RemoteBrowserLauncher_test.go
@@ -19,7 +19,6 @@ func TestRemoteBrowserLauncherConnectsToRealBrowserFromHTTPAndWebSocketCDPEndpoi
}
local, err := NewLocalBrowserLauncher(LaunchOptions{}).Launch(LaunchOptions{
Headless: boolPtr(true),
- Sandbox: boolPtr(false),
Port: port,
})
if err != nil {
diff --git a/go/modcdp/modcdp.go b/go/modcdp/modcdp.go
index 8e8cfd0..e744b01 100644
--- a/go/modcdp/modcdp.go
+++ b/go/modcdp/modcdp.go
@@ -80,7 +80,6 @@ const UpstreamModeNATS = transport.UpstreamModeNATS
const UpstreamEndpointKindRawCDP = transport.UpstreamEndpointKindRawCDP
const UpstreamEndpointKindModCDPServer = transport.UpstreamEndpointKindModCDPServer
const DefaultModCDPExtensionID = injector.DefaultModCDPExtensionID
-const DefaultModCDPWakePath = injector.DefaultModCDPWakePath
const DefaultUpstreamReverseWSBind = transport.DefaultUpstreamReverseWSBind
const DefaultUpstreamReverseWSWaitTimeoutMS = transport.DefaultUpstreamReverseWSWaitTimeoutMS
const DefaultUpstreamNATSWaitTimeoutMS = transport.DefaultUpstreamNATSWaitTimeoutMS
diff --git a/go/modcdp/router/AutoSessionRouter_test.go b/go/modcdp/router/AutoSessionRouter_test.go
index e41fb9d..1ba7fe2 100644
--- a/go/modcdp/router/AutoSessionRouter_test.go
+++ b/go/modcdp/router/AutoSessionRouter_test.go
@@ -107,10 +107,8 @@ func TestAutoSessionRouterBoundsDetachedSessionGuardsAndClearsThemWhenSessionRea
func TestAutoSessionRouterTracksRealTargetSessionsAndExecutionContexts(t *testing.T) {
headless := true
- sandbox := false
chrome, err := launcher.NewLocalBrowserLauncher(launcher.LaunchOptions{
Headless: &headless,
- Sandbox: &sandbox,
}).Launch(launcher.LaunchOptions{})
if err != nil {
t.Fatal(err)
@@ -163,7 +161,7 @@ func TestAutoSessionRouterTracksRealTargetSessionsAndExecutionContexts(t *testin
return nil, fmt.Errorf("%s timed out", method)
}
}
- router = NewAutoSessionRouter(send, func() int { return 5000 })
+ router = NewAutoSessionRouter(send, func() int { return 30000 })
go func() {
for {
@@ -219,7 +217,7 @@ func TestAutoSessionRouterTracksRealTargetSessionsAndExecutionContexts(t *testin
contextResult := make(chan int, 1)
contextError := make(chan error, 1)
go func() {
- contextID, err := router.WaitForExecutionContext(sessionID, 5000)
+ contextID, err := router.WaitForExecutionContext(sessionID, 30000)
if err != nil {
contextError <- err
return
@@ -236,7 +234,7 @@ func TestAutoSessionRouterTracksRealTargetSessionsAndExecutionContexts(t *testin
}
case err := <-contextError:
t.Fatal(err)
- case <-time.After(10 * time.Second):
+ case <-time.After(35 * time.Second):
t.Fatal("timed out waiting for execution context")
}
if _, err := send("Target.detachFromTarget", map[string]any{"sessionId": sessionID}, ""); err != nil {
diff --git a/go/modcdp/translate/translate.go b/go/modcdp/translate/translate.go
index 1f1d187..12bde56 100644
--- a/go/modcdp/translate/translate.go
+++ b/go/modcdp/translate/translate.go
@@ -135,10 +135,10 @@ func wrapModCDPAddMiddleware(params map[string]any) map[string]any {
}
func wrapCustomCommand(method string, params map[string]any, sessionID string) map[string]any {
- m, _ := json.Marshal(method)
p, _ := json.Marshal(params)
- sid, _ := json.Marshal(sessionID)
- return callFunctionParams(fmt.Sprintf(`async function() { return JSON.stringify(await globalThis.ModCDP.handleCommand(%s, %s, %s)); }`, string(m), string(p), string(sid)))
+ runtimeParams := callFunctionParams(`async function(method, paramsJson, cdpSessionId) { return JSON.stringify(await globalThis.ModCDP.handleCommand(method, JSON.parse(paramsJson), cdpSessionId)); }`)
+ runtimeParams["arguments"] = []map[string]any{{"value": method}, {"value": string(p)}, {"value": sessionID}}
+ return runtimeParams
}
func wrapServiceWorkerCommand(method string, params map[string]any, sessionID string, targetSessionID string) []rawStep {
diff --git a/go/modcdp/translate/translate_test.go b/go/modcdp/translate/translate_test.go
index 768347d..df05a5b 100644
--- a/go/modcdp/translate/translate_test.go
+++ b/go/modcdp/translate/translate_test.go
@@ -52,6 +52,37 @@ func TestTranslateRoutesWrapsAndUnwrapsModCDPProtocolMessagesDeterministically(t
t.Fatalf("configure unwrap = %q", configured.Steps[0].Unwrap)
}
+ custom, err := wrapCommandIfNeeded(
+ "Custom.echo",
+ map[string]any{"secret": strings.Repeat("x", 100), "nested": map[string]any{"ok": true}},
+ DefaultClientRoutes(),
+ "session-1",
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ customParams := custom.Steps[0].Params
+ if !strings.Contains(stringValue(customParams["functionDeclaration"]), "JSON.parse(paramsJson)") {
+ t.Fatalf("functionDeclaration = %s", customParams["functionDeclaration"])
+ }
+ if strings.Contains(stringValue(customParams["functionDeclaration"]), "xxxxxxxxxx") {
+ t.Fatalf("functionDeclaration includes custom params: %s", customParams["functionDeclaration"])
+ }
+ customArguments := customParams["arguments"].([]map[string]any)
+ if customArguments[0]["value"] != "Custom.echo" {
+ t.Fatalf("method argument = %#v", customArguments[0])
+ }
+ var customPayload map[string]any
+ if err := json.Unmarshal([]byte(customArguments[1]["value"].(string)), &customPayload); err != nil {
+ t.Fatal(err)
+ }
+ if customPayload["secret"] != strings.Repeat("x", 100) || customPayload["nested"].(map[string]any)["ok"] != true {
+ t.Fatalf("params argument = %#v", customPayload)
+ }
+ if customArguments[2]["value"] != "session-1" {
+ t.Fatalf("session argument = %#v", customArguments[2])
+ }
+
unwrapped, err := unwrapResponseIfNeeded(map[string]any{"result": map[string]any{"type": "object", "value": map[string]any{"ok": true}}}, "runtime")
if err != nil {
t.Fatal(err)
diff --git a/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go b/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go
index 0d0cc37..daa108e 100644
--- a/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go
+++ b/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go
@@ -220,17 +220,31 @@ func waitForNativePeerDisconnect(t *testing.T, transport *NativeMessagingUpstrea
func TestNativeMessagingUpstreamTransportInstallsLaunchProfileManifestAndConnectsToRealExtension(t *testing.T) {
nativeHostName := "com.modcdp.bridge"
headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == ""
- sandbox := runtime.GOOS != "linux"
+ extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ profileDir, err := os.MkdirTemp("", "modcdp.native.")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(profileDir)
cdp := modcdp.New(modcdp.Options{
Launcher: modcdp.LauncherConfig{LauncherMode: "local",
LauncherOptions: modcdp.LaunchOptions{
- Headless: boolPtr(headless),
- Sandbox: boolPtr(sandbox),
+ Headless: boolPtr(headless),
+ UserDataDir: profileDir,
+ CleanupUserDataDir: boolPtr(true),
+ // Native messaging is browser -> client only. After explicit CHROME_PATH
+ // and CI /usr/bin/chromium, this test uses Chrome for Testing because
+ // Canary rejects --load-extension in this local test path.
+ ExecutablePath: reverseWSTestBrowserPath(t),
},
},
Upstream: modcdp.UpstreamConfig{UpstreamMode: "nativemessaging", UpstreamNativeMessagingHostName: nativeHostName},
Injector: modcdp.InjectorConfig{
InjectorMode: "auto",
+ InjectorExtensionPath: extensionPath,
InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
InjectorTrustServiceWorkerTarget: true,
},
diff --git a/go/modcdp/transport/PipeUpstreamTransport_test.go b/go/modcdp/transport/PipeUpstreamTransport_test.go
index 6332e47..b1fa9a2 100644
--- a/go/modcdp/transport/PipeUpstreamTransport_test.go
+++ b/go/modcdp/transport/PipeUpstreamTransport_test.go
@@ -4,8 +4,8 @@ import (
modcdp "github.com/browserbase/modcdp/go/modcdp/client"
. "github.com/browserbase/modcdp/go/modcdp/transport"
"os"
+ "path/filepath"
"regexp"
- "runtime"
"testing"
"time"
)
@@ -25,7 +25,7 @@ func TestPipeUpstreamTransportConstructorUpdateLauncherConfigAndUnconnectedError
if err := transport.Connect(); err == nil {
t.Fatal("expected Connect to require pipe handles")
}
- if err := transport.Send(map[string]any{"id": 1, "method": "Browser.getVersion"}); err == nil {
+ if err := transport.Send(map[string]any{"id": 1, "method": "Runtime.evaluate"}); err == nil {
t.Fatal("expected Send to require a connected pipe")
}
}
@@ -54,7 +54,7 @@ func TestPipeUpstreamTransportResetsConnectionStateAfterPipeCloses(t *testing.T)
if err := transport.Connect(); err != nil {
t.Fatal(err)
}
- if err := transport.Send(map[string]any{"id": 1, "method": "Browser.getVersion"}); err != nil {
+ if err := transport.Send(map[string]any{"id": 1, "method": "Runtime.evaluate", "params": map[string]any{"expression": "1"}}); err != nil {
t.Fatal(err)
}
_ = pipeReadWriter.Close()
@@ -63,27 +63,30 @@ func TestPipeUpstreamTransportResetsConnectionStateAfterPipeCloses(t *testing.T)
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for pipe close")
}
- if err := transport.Send(map[string]any{"id": 2, "method": "Browser.getVersion"}); err == nil {
+ if err := transport.Send(map[string]any{"id": 2, "method": "Runtime.evaluate", "params": map[string]any{"expression": "1"}}); err == nil {
t.Fatal("expected send to fail after pipe close")
}
}
func TestPipeUpstreamTransportLaunchesRealBrowserAndUsesPIDScopedPipeURL(t *testing.T) {
- headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == ""
- sandbox := runtime.GOOS != "linux"
+ extensionPath, err := filepath.Abs("../../../dist/extension")
+ if err != nil {
+ t.Fatal(err)
+ }
cdp := modcdp.New(modcdp.Options{
Launcher: modcdp.LauncherConfig{LauncherMode: "local",
LauncherOptions: modcdp.LaunchOptions{
- Headless: boolPtr(headless),
- Sandbox: boolPtr(sandbox),
+ Headless: boolPtr(true),
},
},
Upstream: modcdp.UpstreamConfig{UpstreamMode: "pipe"},
Injector: modcdp.InjectorConfig{
- InjectorMode: "auto",
+ InjectorMode: "inject",
+ InjectorExtensionPath: extensionPath,
InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
InjectorTrustServiceWorkerTarget: true,
},
+ Server: &modcdp.ServerConfig{ServerRoutes: map[string]string{"*.*": "chrome_debugger"}},
})
defer cdp.Close()
@@ -106,11 +109,22 @@ func TestPipeUpstreamTransportLaunchesRealBrowserAndUsesPIDScopedPipeURL(t *test
if pipeTransport.URL != cdp.CDPURL {
t.Fatalf("pipe transport URL = %q, CDPURL = %q", pipeTransport.URL, cdp.CDPURL)
}
- version, err := cdp.SendRaw("Browser.getVersion", map[string]any{})
+ if _, err := cdp.Mod.AddCustomCommand(modcdp.CustomCommand{
+ Name: "Custom.runtimeReadyState",
+ Expression: "async () => await cdp.send('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true })",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ runtime, err := cdp.Send("Custom.runtimeReadyState", nil)
if err != nil {
t.Fatal(err)
}
- if _, ok := version["product"].(string); !ok {
- t.Fatalf("Browser.getVersion product = %#v", version["product"])
+ runtimeMap, ok := runtime.(map[string]any)
+ if !ok {
+ t.Fatalf("Custom.runtimeReadyState = %#v", runtime)
+ }
+ runtimeResult, _ := runtimeMap["result"].(map[string]any)
+ if runtimeResult["value"] != "complete" {
+ t.Fatalf("Runtime.evaluate result = %#v", runtime)
}
}
diff --git a/go/modcdp/transport/ReverseWebSocketUpstreamTransport.go b/go/modcdp/transport/ReverseWebSocketUpstreamTransport.go
index 2c470d7..d733e25 100644
--- a/go/modcdp/transport/ReverseWebSocketUpstreamTransport.go
+++ b/go/modcdp/transport/ReverseWebSocketUpstreamTransport.go
@@ -112,7 +112,7 @@ func (t *ReverseWebSocketUpstreamTransport) Send(message map[string]any) error {
}
func (t *ReverseWebSocketUpstreamTransport) GetInjectorConfig() ExtensionInjectorConfig {
- return ExtensionInjectorConfig{UpstreamReverseWSURL: t.URL}
+ return ExtensionInjectorConfig{}
}
func (t *ReverseWebSocketUpstreamTransport) WaitForPeer() error {
diff --git a/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go b/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go
index cc6de18..a50b6dd 100644
--- a/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go
+++ b/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go
@@ -7,7 +7,12 @@ import (
modcdp "github.com/browserbase/modcdp/go/modcdp/client"
. "github.com/browserbase/modcdp/go/modcdp/transport"
"os"
+ "path/filepath"
+ "reflect"
+ "regexp"
"runtime"
+ "sort"
+ "strconv"
"strings"
"testing"
"time"
@@ -16,19 +21,19 @@ import (
"github.com/gobwas/ws/wsutil"
)
-func TestReverseWebSocketUpstreamTransportConfigOwnsBindUpdatesWaitTimeoutAndInjectorConfig(t *testing.T) {
+func TestReverseWebSocketUpstreamTransportConfigOwnsBindUpdatesAndWaitTimeout(t *testing.T) {
transport := NewReverseWebSocketUpstreamTransport(ReverseWebSocketUpstreamTransportOptions{UpstreamReverseWSBind: "127.0.0.1:29292", UpstreamReverseWSWaitTimeoutMS: 10})
if transport.URL != "ws://127.0.0.1:29292" {
t.Fatalf("URL = %q", transport.URL)
}
- if transport.GetInjectorConfig().UpstreamReverseWSURL != "ws://127.0.0.1:29292" {
+ if !reflect.DeepEqual(transport.GetInjectorConfig(), ExtensionInjectorConfig{}) {
t.Fatalf("injector config = %#v", transport.GetInjectorConfig())
}
transport.Update(map[string]any{"upstream_reversews_bind": "127.0.0.1:29293", "upstream_reversews_wait_timeout_ms": 5})
if transport.URL != "ws://127.0.0.1:29293" {
t.Fatalf("URL after update = %q", transport.URL)
}
- if transport.GetInjectorConfig().UpstreamReverseWSURL != "ws://127.0.0.1:29293" {
+ if !reflect.DeepEqual(transport.GetInjectorConfig(), ExtensionInjectorConfig{}) {
t.Fatalf("injector config after update = %#v", transport.GetInjectorConfig())
}
transport.Update(map[string]any{"upstream_reversews_bind": "http://127.0.0.1:29294"})
@@ -214,23 +219,30 @@ func waitForReversePeerDisconnect(t *testing.T, transport *ReverseWebSocketUpstr
t.Fatal("timed out waiting for reverse peer disconnect")
}
-func TestReverseWebSocketUpstreamTransportAcceptsRealExtensionReverseConnectionAndRoutesCDPThroughLoopback(t *testing.T) {
+func TestReverseWebSocketUpstreamTransportAcceptsRealExtensionReverseConnectionAndRoutesCDPThroughChromeDebugger(t *testing.T) {
+ extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
+ if err != nil {
+ t.Fatal(err)
+ }
headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == ""
- sandbox := runtime.GOOS != "linux"
cdp := modcdp.New(modcdp.Options{
Launcher: modcdp.LauncherConfig{LauncherMode: "local",
LauncherOptions: modcdp.LaunchOptions{
Headless: boolPtr(headless),
- Sandbox: boolPtr(sandbox),
+ // Reversews is browser -> client only. After explicit CHROME_PATH and
+ // CI /usr/bin/chromium, these tests use Chrome for Testing because
+ // Canary rejects --load-extension in this local test path.
+ ExecutablePath: reverseWSTestBrowserPath(t),
},
},
Upstream: modcdp.UpstreamConfig{UpstreamMode: "reversews"},
Injector: modcdp.InjectorConfig{
- InjectorMode: "auto",
- InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
- InjectorTrustServiceWorkerTarget: true,
+ InjectorExtensionPath: extensionPath,
+ InjectorMode: "auto",
+ InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"},
+ InjectorTrustServiceWorkerTarget: true,
+ InjectorServiceWorkerProbeTimeoutMS: 1000,
},
- Server: &modcdp.ServerConfig{ServerRoutes: map[string]string{"*.*": "loopback_cdp"}},
})
defer cdp.Close()
@@ -247,30 +259,137 @@ func TestReverseWebSocketUpstreamTransportAcceptsRealExtensionReverseConnectionA
if !ok {
t.Fatalf("transport = %T", cdp.Transport())
}
+ if transport.URL != "ws://127.0.0.1:29292" {
+ t.Fatalf("transport URL = %q", transport.URL)
+ }
if transport.PeerInfo["extension_id"] != DefaultModCDPExtensionID {
t.Fatalf("extension_id = %#v", transport.PeerInfo["extension_id"])
}
- result, err := cdp.Send("Browser.getVersion", map[string]any{})
+ result, err := cdp.Send("Runtime.evaluate", map[string]any{"expression": "location.href", "returnByValue": true})
if err != nil {
t.Fatal(err)
}
- version, ok := result.(map[string]any)
+ evaluated, ok := result.(map[string]any)
if !ok {
- t.Fatalf("Browser.getVersion result = %#v", result)
+ t.Fatalf("Runtime.evaluate result = %#v", result)
}
- if _, ok := version["product"].(string); !ok {
- t.Fatalf("Browser.getVersion product = %#v", version["product"])
+ evaluatedResult, _ := evaluated["result"].(map[string]any)
+ if evaluatedResult["value"] != "about:blank" {
+ t.Fatalf("Runtime.evaluate value = %#v", evaluatedResult["value"])
}
time.Sleep(1500 * time.Millisecond)
- secondResult, err := cdp.Send("Browser.getVersion", map[string]any{})
+ secondResult, err := cdp.Send("Runtime.evaluate", map[string]any{"expression": "document.readyState", "returnByValue": true})
if err != nil {
t.Fatal(err)
}
- secondVersion, ok := secondResult.(map[string]any)
+ secondEvaluated, ok := secondResult.(map[string]any)
if !ok {
- t.Fatalf("second Browser.getVersion result = %#v", secondResult)
+ t.Fatalf("second Runtime.evaluate result = %#v", secondResult)
+ }
+ secondEvaluatedResult, _ := secondEvaluated["result"].(map[string]any)
+ if secondEvaluatedResult["value"] != "complete" {
+ t.Fatalf("second Runtime.evaluate value = %#v", secondEvaluatedResult["value"])
+ }
+}
+
+func reverseWSTestBrowserPath(t *testing.T) string {
+ t.Helper()
+ explicitCandidates := []string{os.Getenv("CHROME_PATH")}
+ if runtime.GOOS == "linux" {
+ explicitCandidates = append(explicitCandidates, "/usr/bin/chromium")
+ }
+ for _, candidate := range explicitCandidates {
+ if candidate == "" {
+ continue
+ }
+ if _, err := os.Stat(candidate); err == nil {
+ return candidate
+ }
}
- if _, ok := secondVersion["product"].(string); !ok {
- t.Fatalf("second Browser.getVersion product = %#v", secondVersion["product"])
+ home, err := os.UserHomeDir()
+ if err != nil || home == "" {
+ home = "."
+ }
+ localAppData := os.Getenv("LOCALAPPDATA")
+ if localAppData == "" {
+ localAppData = filepath.Join(home, "AppData", "Local")
+ }
+ var patterns []string
+ switch runtime.GOOS {
+ case "darwin":
+ patterns = []string{
+ filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"),
+ filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Chromium.app", "Contents", "MacOS", "Chromium"),
+ filepath.Join(home, "Library", "Caches", "puppeteer", "chrome", "mac*-*", "chrome-mac*", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"),
+ }
+ case "windows":
+ patterns = []string{
+ filepath.Join(localAppData, "ms-playwright", "chromium-*", "chrome-win*", "chrome.exe"),
+ filepath.Join(home, ".cache", "puppeteer", "chrome", "win*-*", "chrome.exe"),
+ }
+ default:
+ patterns = []string{
+ filepath.Join(home, ".cache", "ms-playwright", "chromium-*", "chrome-linux*", "chrome"),
+ filepath.Join("/opt", "pw-browsers", "chromium-*", "chrome-linux*", "chrome"),
+ filepath.Join(home, ".cache", "puppeteer", "chrome", "linux-*", "chrome-linux*", "chrome"),
+ }
+ }
+ var candidates []string
+ for _, pattern := range patterns {
+ matches, err := filepath.Glob(pattern)
+ if err != nil {
+ continue
+ }
+ candidates = append(candidates, matches...)
+ }
+ candidates = newestChromeForTestingFirst(candidates)
+ if len(candidates) > 0 {
+ return candidates[0]
+ }
+ t.Fatal("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.")
+ return ""
+}
+
+func newestChromeForTestingFirst(candidates []string) []string {
+ seen := map[string]bool{}
+ deduped := make([]string, 0, len(candidates))
+ for _, candidate := range candidates {
+ if candidate == "" || seen[candidate] {
+ continue
+ }
+ seen[candidate] = true
+ deduped = append(deduped, candidate)
+ }
+ sort.SliceStable(deduped, func(i, j int) bool {
+ leftVersion := maxPathNumber(deduped[i])
+ rightVersion := maxPathNumber(deduped[j])
+ if leftVersion != rightVersion {
+ return leftVersion > rightVersion
+ }
+ leftStat, leftErr := os.Stat(deduped[i])
+ rightStat, rightErr := os.Stat(deduped[j])
+ var leftMtime, rightMtime time.Time
+ if leftErr == nil {
+ leftMtime = leftStat.ModTime()
+ }
+ if rightErr == nil {
+ rightMtime = rightStat.ModTime()
+ }
+ if !leftMtime.Equal(rightMtime) {
+ return leftMtime.After(rightMtime)
+ }
+ return deduped[i] < deduped[j]
+ })
+ return deduped
+}
+
+func maxPathNumber(value string) int {
+ maxValue := 0
+ for _, raw := range regexp.MustCompile(`\d+`).FindAllString(value, -1) {
+ number, err := strconv.Atoi(raw)
+ if err == nil && number > maxValue {
+ maxValue = number
+ }
}
+ return maxValue
}
diff --git a/go/modcdp/transport/WebSocketUpstreamTransport_test.go b/go/modcdp/transport/WebSocketUpstreamTransport_test.go
index b93e5e8..5892ddf 100644
--- a/go/modcdp/transport/WebSocketUpstreamTransport_test.go
+++ b/go/modcdp/transport/WebSocketUpstreamTransport_test.go
@@ -37,7 +37,6 @@ func TestWebSocketUpstreamTransportLaunchesRealBrowserAndSpeaksRawCDP(t *testing
Launcher: modcdp.LauncherConfig{LauncherMode: "local",
LauncherOptions: modcdp.LaunchOptions{
Headless: boolPtr(true),
- Sandbox: boolPtr(false),
},
},
Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws"},
@@ -115,7 +114,6 @@ func TestWebSocketUpstreamTransportLaunchesRealBrowserAndSpeaksRawCDP(t *testing
func TestWebSocketUpstreamTransportResolvesRealHTTPCDPEndpointToBrowserWebSocket(t *testing.T) {
chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
Headless: boolPtr(true),
- Sandbox: boolPtr(false),
}).Launch(modcdp.LaunchOptions{})
if err != nil {
t.Fatal(err)
@@ -161,7 +159,6 @@ func TestWebSocketUpstreamTransportResolvesRealHTTPCDPEndpointToBrowserWebSocket
func TestWebSocketUpstreamTransportCloseClearsConnectionState(t *testing.T) {
chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{
Headless: boolPtr(true),
- Sandbox: boolPtr(false),
}).Launch(modcdp.LaunchOptions{})
if err != nil {
t.Fatal(err)
diff --git a/go/modcdp/types/types.go b/go/modcdp/types/types.go
index cf2c8a9..eb446f7 100644
--- a/go/modcdp/types/types.go
+++ b/go/modcdp/types/types.go
@@ -39,8 +39,6 @@ type ExtensionInjectorConfig struct {
WaitForExecutionContext WaitForExecutionContext `json:"-"`
InjectorExtensionPath string `json:"injector_extension_path,omitempty"`
InjectorExtensionID string `json:"injector_extension_id,omitempty"`
- InjectorWakePath string `json:"injector_wake_path,omitempty"`
- InjectorWakeURL string `json:"injector_wake_url,omitempty"`
InjectorServiceWorkerURLIncludes []string `json:"injector_service_worker_url_includes,omitempty"`
InjectorServiceWorkerURLSuffixes []string `json:"injector_service_worker_url_suffixes,omitempty"`
InjectorTrustServiceWorkerTarget bool `json:"injector_trust_service_worker_target,omitempty"`
@@ -54,7 +52,6 @@ type ExtensionInjectorConfig struct {
InjectorTargetSessionPollIntervalMS int `json:"injector_target_session_poll_interval_ms,omitempty"`
InjectorBrowserbaseAPIKey string `json:"injector_browserbase_api_key,omitempty"`
InjectorBrowserbaseBaseURL string `json:"injector_browserbase_base_url,omitempty"`
- UpstreamReverseWSURL string `json:"upstream_reversews_url,omitempty"`
UpstreamNativeMessagingHostName string `json:"upstream_nativemessaging_host_name,omitempty"`
UpstreamNATSURL string `json:"upstream_nats_url,omitempty"`
UpstreamNATSSubjectPrefix string `json:"upstream_nats_subject_prefix,omitempty"`
diff --git a/js/examples/demo.ts b/js/examples/demo.ts
index 5d4ee2e..f676383 100644
--- a/js/examples/demo.ts
+++ b/js/examples/demo.ts
@@ -42,10 +42,11 @@ const EXTENSION_PATH =
existsSync(path.join(candidate, "modcdp/service_worker.js")),
) ?? path.resolve(HERE, "..", "..", "extension");
const DEFAULT_DEMO_EVENT_TIMEOUT_MS = 10_000;
+const DEFAULT_REVERSE_TRANSPORT_WAIT_TIMEOUT_MS = 60_000;
const DEFAULT_LIVE_CDP_POLL_INTERVAL_MS = 250;
const DEFAULT_LIVE_CDP_ACTIVE_PORT_STALE_MS = 1_000;
const DEFAULT_TARGET_EVENT_TIMEOUT_MS = 10_000;
-const DEFAULT_FOREGROUND_EVENT_TIMEOUT_MS = 10_000;
+const DEFAULT_PAGE_TARGET_EVENT_TIMEOUT_MS = 10_000;
const DEFAULT_DEMO_EVENT_POLL_INTERVAL_MS = 20;
const UPSTREAM_MODES = new Set(["ws", "pipe", "reversews", "nativemessaging", "nats"]);
@@ -121,7 +122,17 @@ function clientOptionsFor(mode, upstream_mode, cdp_url, launch_options = {}) {
const launcher = cdp_url
? ({ launcher_mode: "remote" } as const)
: ({ launcher_mode: "local", launcher_options: launch_options } as const);
- const upstream = { upstream_mode, upstream_cdp_url: cdp_url };
+ const upstream = {
+ upstream_mode,
+ upstream_cdp_url: cdp_url,
+ ...(upstream_mode === "reversews"
+ ? { upstream_reversews_wait_timeout_ms: DEFAULT_REVERSE_TRANSPORT_WAIT_TIMEOUT_MS }
+ : {}),
+ ...(upstream_mode === "nativemessaging"
+ ? { upstream_nativemessaging_wait_timeout_ms: DEFAULT_REVERSE_TRANSPORT_WAIT_TIMEOUT_MS }
+ : {}),
+ ...(upstream_mode === "nats" ? { upstream_nats_wait_timeout_ms: DEFAULT_REVERSE_TRANSPORT_WAIT_TIMEOUT_MS } : {}),
+ };
const injector = {
injector_mode: "auto" as const,
injector_extension_path: EXTENSION_PATH,
@@ -239,13 +250,14 @@ async function main() {
} else {
cdp_url = null;
launch_options = {
+ chrome_ready_timeout_ms: 60_000,
headless: process.platform === "linux" && !process.env.DISPLAY,
sandbox: process.platform !== "linux",
};
}
const cdp = new ModCDPClient(clientOptionsFor(mode, upstream_mode, cdp_url, launch_options));
- const foregroundEvents = [];
+ const pageTargetEvents = [];
const targetCreatedEvents: TargetCreatedPayload[] = [];
try {
@@ -461,35 +473,24 @@ async function main() {
const demoEvent = assertObject(await demoEventPromise, "Custom.demoEvent");
console.log("Custom.demoEvent ->", demoEvent);
- const ForegroundTargetChanged = z
+ const PageTargetUpdated = z
.object({
- targetId: cdp.types.zod.Target.TargetID.nullable(),
- tabId: z.number(),
+ targetId: cdp.types.zod.Target.TargetID,
+ tabId: z.number().optional(),
url: z.string().nullable().optional(),
})
.passthrough()
- .meta({ id: "Custom.foregroundTargetChanged" });
- const foregroundEventRegistration = assertObject(
- await cdp.Mod.addCustomEvent(ForegroundTargetChanged),
- "Mod.addCustomEvent Custom.foregroundTargetChanged",
+ .meta({ id: "Custom.pageTargetUpdated" });
+ const pageTargetEventRegistration = assertObject(
+ await cdp.Mod.addCustomEvent(PageTargetUpdated),
+ "Mod.addCustomEvent Custom.pageTargetUpdated",
);
- if (foregroundEventRegistration.registered !== true) {
- throw new Error(`unexpected foreground event registration ${JSON.stringify(foregroundEventRegistration)}`);
+ if (pageTargetEventRegistration.registered !== true) {
+ throw new Error(`unexpected page target event registration ${JSON.stringify(pageTargetEventRegistration)}`);
}
- cdp.on(ForegroundTargetChanged, (event) => {
- console.log("Custom.foregroundTargetChanged ->", event);
- foregroundEvents.push(event);
- });
- await cdp.Mod.evaluate({
- expression: `async () => {
- chrome.tabs.onActivated.addListener(async ({ tabId }) => {
- const targets = await chrome.debugger.getTargets();
- const target = targets.find(target => target.type === "page" && target.tabId === tabId);
- const tab = await chrome.tabs.get(tabId).catch(() => null);
- await cdp.emit("Custom.foregroundTargetChanged", { tabId, targetId: target?.id ?? null, url: target?.url ?? tab?.url ?? null });
- });
- return true;
- }`,
+ cdp.on(PageTargetUpdated, (event) => {
+ console.log("Custom.pageTargetUpdated ->", event);
+ pageTargetEvents.push(event);
});
await cdp.Target.setDiscoverTargets({ discover: true });
@@ -523,22 +524,38 @@ async function main() {
console.log("Custom.TabIdFromTargetId ->", tabFromTarget);
await cdp.Target.activateTarget({ targetId: createdTarget.targetId });
- const foregroundDeadline = Date.now() + DEFAULT_FOREGROUND_EVENT_TIMEOUT_MS;
+ const pageTargetEmitResult = assertObject(
+ await cdp.Mod.evaluate({
+ params: { targetId: createdTarget.targetId },
+ expression: `async ({ targetId }) => {
+ const targets = await chrome.debugger.getTargets();
+ const target = targets.find(target => target.id === targetId);
+ if (!target?.id) throw new Error(\`target \${targetId} not found\`);
+ await cdp.emit("Custom.pageTargetUpdated", { targetId: target.id, url: target.url ?? null });
+ return { emitted: true, targetId: target.id };
+ }`,
+ }),
+ "Custom.pageTargetUpdated emit",
+ );
+ if (pageTargetEmitResult.emitted !== true || pageTargetEmitResult.targetId !== createdTarget.targetId) {
+ throw new Error(`unexpected Custom.pageTargetUpdated emit result ${JSON.stringify(pageTargetEmitResult)}`);
+ }
+ const pageTargetDeadline = Date.now() + DEFAULT_PAGE_TARGET_EVENT_TIMEOUT_MS;
while (
- !foregroundEvents.some((event) => event.targetId === createdTarget.targetId) &&
- Date.now() < foregroundDeadline
+ !pageTargetEvents.some((event) => event.targetId === createdTarget.targetId) &&
+ Date.now() < pageTargetDeadline
) {
await sleep(DEFAULT_DEMO_EVENT_POLL_INTERVAL_MS);
}
- const foreground = foregroundEvents.find((event) => event.targetId === createdTarget.targetId);
- if (!foreground) throw new Error(`expected Custom.foregroundTargetChanged for ${createdTarget.targetId}`);
- if (foreground.tabId !== tabFromTargetId)
- throw new Error(`unexpected Custom.foregroundTargetChanged result ${JSON.stringify(foreground)}`);
+ const pageTarget = pageTargetEvents.find((event) => event.targetId === createdTarget.targetId);
+ if (!pageTarget) throw new Error(`expected Custom.pageTargetUpdated for ${createdTarget.targetId}`);
+ if (pageTarget.tabId !== tabFromTargetId)
+ throw new Error(`unexpected Custom.pageTargetUpdated result ${JSON.stringify(pageTarget)}`);
const targetFromTab = await cdp.send("Custom.targetIdFromTabId", {
- tabId: foreground.tabId,
+ tabId: pageTarget.tabId,
});
- if (targetFromTab.targetId !== createdTarget.targetId || targetFromTab.tabId !== foreground.tabId) {
+ if (targetFromTab.targetId !== createdTarget.targetId || targetFromTab.tabId !== pageTarget.tabId) {
throw new Error(`unexpected Custom.targetIdFromTabId/middleware result ${JSON.stringify(targetFromTab)}`);
}
console.log("Custom.targetIdFromTabId ->", targetFromTab);
diff --git a/js/examples/playwright.ts b/js/examples/playwright.ts
index 265e89e..b868ccf 100644
--- a/js/examples/playwright.ts
+++ b/js/examples/playwright.ts
@@ -26,6 +26,7 @@ let browser: Awaited> | null = null;
try {
chrome = await new LocalBrowserLauncher().launch({
+ chrome_ready_timeout_ms: 60_000,
headless: process.platform === "linux" && !process.env.DISPLAY,
sandbox: process.platform !== "linux",
extra_args: [`--load-extension=${extension_path}`],
diff --git a/js/examples/puppeteer.ts b/js/examples/puppeteer.ts
index e0785ba..0aa8c0a 100644
--- a/js/examples/puppeteer.ts
+++ b/js/examples/puppeteer.ts
@@ -26,6 +26,7 @@ let browser: Awaited> | null = null;
try {
chrome = await new LocalBrowserLauncher().launch({
+ chrome_ready_timeout_ms: 60_000,
headless: process.platform === "linux" && !process.env.DISPLAY,
sandbox: process.platform !== "linux",
extra_args: [`--load-extension=${extension_path}`],
diff --git a/js/scripts/build-extension-assets.mjs b/js/scripts/build-extension-assets.mjs
index f267968..aa957c9 100755
--- a/js/scripts/build-extension-assets.mjs
+++ b/js/scripts/build-extension-assets.mjs
@@ -3,6 +3,7 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
+import { build } from "esbuild";
const root = process.cwd();
const dist_extension = path.join(root, "dist", "extension");
@@ -18,28 +19,21 @@ const copy = (from, to) => {
fs.copyFileSync(path.join(root, from), path.join(root, to));
};
-const write = (to, contents) => {
- fs.mkdirSync(path.dirname(path.join(root, to)), { recursive: true });
- fs.writeFileSync(path.join(root, to), contents);
-};
-
copy("extension/manifest.json", "dist/extension/manifest.json");
copy("extension/src/pages/options.html", "dist/extension/options.html");
copy("dist/extension/src/pages/options.js", "dist/extension/options.js");
-copy("extension/src/pages/wake.html", "dist/extension/modcdp/wake.html");
-copy("dist/extension/src/pages/wake.js", "dist/extension/modcdp/wake.js");
copy("extension/src/pages/offscreen_keepalive.html", "dist/extension/offscreen/keepalive.html");
copy("dist/extension/src/pages/offscreen_keepalive.js", "dist/extension/offscreen/offscreen_keepalive.js");
-copy("dist/js/src/server/ModCDPServer.js", "dist/extension/js/src/server/ModCDPServer.js");
-if (fs.existsSync(path.join(root, "dist/js/src/server/ModCDPServer.js.map"))) {
- copy("dist/js/src/server/ModCDPServer.js.map", "dist/extension/js/src/server/ModCDPServer.js.map");
-}
-
-const service_worker = fs
- .readFileSync(path.join(root, "dist/extension/src/service_worker.js"), "utf8")
- .replace("../../js/src/server/ModCDPServer.js", "../js/src/server/ModCDPServer.js");
-write("dist/extension/modcdp/service_worker.js", service_worker);
-copy("dist/extension/src/service_worker.js.map", "dist/extension/modcdp/service_worker.js.map");
+await build({
+ entryPoints: [path.join(root, "extension/src/service_worker.ts")],
+ outfile: path.join(root, "dist/extension/modcdp/service_worker.js"),
+ bundle: true,
+ format: "esm",
+ platform: "browser",
+ target: ["chrome116"],
+ sourcemap: true,
+ logLevel: "silent",
+});
fs.rmSync(path.join(dist_extension, "src"), { recursive: true, force: true });
const fixed_date = new Date("2000-01-01T00:00:00Z");
diff --git a/js/src/client/ModCDPClient.ts b/js/src/client/ModCDPClient.ts
index 2e136c7..01462f0 100644
--- a/js/src/client/ModCDPClient.ts
+++ b/js/src/client/ModCDPClient.ts
@@ -14,6 +14,7 @@ import type { z } from "zod";
import { createCdpAliases, type CdpAliases } from "../types/generated/aliases.js";
export type { CdpAliases } from "../types/generated/aliases.js";
+import { commands as nativeCommandSchemas, events as nativeEventSchemas } from "../types/generated/zod.js";
import {
CUSTOM_EVENT_BINDING_NAME,
DEFAULT_CLIENT_ROUTES,
@@ -69,7 +70,6 @@ export const DEFAULT_SERVICE_WORKER_POLL_INTERVAL_MS = 100;
export const DEFAULT_TARGET_SESSION_POLL_INTERVAL_MS = 20;
export const DEFAULT_WS_CONNECT_ERROR_SETTLE_TIMEOUT_MS = 250;
export const DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES = ["/modcdp/service_worker.js"];
-export const DEFAULT_MODCDP_WAKE_PATH = "/modcdp/wake.html";
export const DEFAULT_UPSTREAM_REVERSEWS_BIND = "127.0.0.1:29292";
export const DEFAULT_UPSTREAM_REVERSEWS_WAIT_TIMEOUT_MS = 10_000;
export const DEFAULT_UPSTREAM_NATIVEMESSAGING_WAIT_TIMEOUT_MS = 10_000;
@@ -106,8 +106,6 @@ export type InjectorOptions = {
injector_mode?: InjectorMode;
injector_extension_path?: string | null;
injector_extension_id?: string | null;
- injector_wake_path?: string | null;
- injector_wake_url?: string | null;
injector_service_worker_url_includes?: string[];
injector_service_worker_url_suffixes?: string[] | null;
injector_trust_service_worker_target?: boolean;
@@ -159,6 +157,10 @@ type ModCDPEventPayload = TEvent extends z.ZodType & {
expression?: string | null;
};
+type ProtocolCommandSchema = {
+ params: z.ZodType;
+ result: z.ZodType;
+};
export type ModCDPCommandSpec = {
params: Params;
@@ -265,8 +267,6 @@ function normalizeClientOptions({
injector_mode,
injector_extension_path: injector.injector_extension_path ?? null,
injector_extension_id: injector.injector_extension_id ?? null,
- injector_wake_path: injector.injector_wake_path ?? DEFAULT_MODCDP_WAKE_PATH,
- injector_wake_url: injector.injector_wake_url ?? null,
injector_service_worker_url_includes: injector.injector_service_worker_url_includes ?? [],
injector_service_worker_url_suffixes:
injector.injector_service_worker_url_suffixes ?? DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES,
@@ -285,7 +285,10 @@ function normalizeClientOptions({
injector.injector_target_session_poll_interval_ms ?? DEFAULT_TARGET_SESSION_POLL_INTERVAL_MS,
},
client: {
- client_routes: { ...DEFAULT_CLIENT_ROUTES, ...(client.client_routes ?? {}) },
+ client_routes: {
+ ...DEFAULT_CLIENT_ROUTES,
+ ...(client.client_routes ?? {}),
+ },
client_hydrate_aliases: client.client_hydrate_aliases ?? true,
client_mirror_upstream_events: client.client_mirror_upstream_events ?? true,
client_cdp_send_timeout_ms: client.client_cdp_send_timeout_ms ?? DEFAULT_CDP_SEND_TIMEOUT_MS,
@@ -466,6 +469,7 @@ export class ModCDPClient extends ModCDPEventEmitter {
on: (event_name: string | symbol, listener: (...args: unknown[]) => void) => this.on(event_name, listener),
once: (event_name: string | symbol, listener: (...args: unknown[]) => void) => this.once(event_name, listener),
};
+ this._hydrateNativeProtocolSchemas();
void this._hydrateCdpAliases();
this._hydrateCustomSurface();
}
@@ -579,7 +583,7 @@ export class ModCDPClient extends ModCDPEventEmitter {
completed_at: Date.now(),
duration_ms: Date.now() - started_at,
};
- return { name, registered: true };
+ return this.command_result_schemas.get(method)?.parse({ name, registered: true }) ?? { name, registered: true };
}
command_params = {
...parsed,
@@ -600,7 +604,7 @@ export class ModCDPClient extends ModCDPEventEmitter {
completed_at: Date.now(),
duration_ms: Date.now() - started_at,
};
- return { name, registered: true };
+ return this.command_result_schemas.get(method)?.parse({ name, registered: true }) ?? { name, registered: true };
}
command_params = { ...parsed, name, event_schema: null };
}
@@ -684,6 +688,30 @@ export class ModCDPClient extends ModCDPEventEmitter {
}
}
+ _hydrateNativeProtocolSchemas() {
+ for (const [method, schema] of Object.entries(nativeCommandSchemas) as [string, ProtocolCommandSchema][]) {
+ this.command_params_schemas.set(method, schema.params);
+ this.command_result_schemas.set(method, schema.result);
+ }
+ this.command_params_schemas.set("Mod.evaluate", Mod.EvaluateParams);
+ this.command_result_schemas.set("Mod.evaluate", Mod.EvaluateResponse);
+ this.command_params_schemas.set("Mod.addCustomCommand", Mod.AddCustomCommandParams);
+ this.command_result_schemas.set("Mod.addCustomCommand", Mod.AddCustomCommandResponse);
+ this.command_params_schemas.set("Mod.addCustomEvent", Mod.AddCustomEventParams);
+ this.command_result_schemas.set("Mod.addCustomEvent", Mod.AddCustomEventResponse);
+ this.command_params_schemas.set("Mod.addMiddleware", Mod.AddMiddlewareParams);
+ this.command_result_schemas.set("Mod.addMiddleware", Mod.AddMiddlewareResponse);
+ this.command_params_schemas.set("Mod.configure", Mod.ConfigureParams);
+ this.command_result_schemas.set("Mod.configure", Mod.ConfigureResponse);
+ this.command_params_schemas.set("Mod.ping", Mod.PingParams);
+ this.command_result_schemas.set("Mod.ping", Mod.PingResponse);
+
+ for (const [event, schema] of Object.entries(nativeEventSchemas) as [string, z.ZodType][]) {
+ this.event_schemas.set(event, schema);
+ }
+ this.event_schemas.set("Mod.pong", Mod.PongEvent);
+ }
+
_normalizePayloadSchema(schema: unknown) {
return normalizeModCDPPayloadSchema(Mod.PayloadSchemaSpec.parse(schema));
}
@@ -695,18 +723,18 @@ export class ModCDPClient extends ModCDPEventEmitter {
else this.command_result_unwrap_keys.delete(name);
}
+ _parseEventPayload(method: string, data: unknown) {
+ return this.event_schemas.get(method)?.parse(data) ?? data;
+ }
+
_serviceWorkerUrlSuffixes() {
return this.injector.injector_service_worker_url_suffixes;
}
_serverConfigureParams(): ModCDPConfigureParams {
- const transport_injector_config = this.transport?.getInjectorConfig?.() ?? {};
return {
upstream: {
upstream_mode: this.upstream.upstream_mode,
- ...(transport_injector_config.upstream_reversews_url
- ? { upstream_reversews_url: transport_injector_config.upstream_reversews_url }
- : {}),
...(this.upstream.upstream_nats_url ? { upstream_nats_url: this.upstream.upstream_nats_url } : {}),
...(this.upstream.upstream_nats_subject_prefix
? {
@@ -888,7 +916,11 @@ export class ModCDPClient extends ModCDPEventEmitter {
async _injectorsForConfig() {
if (this.injector.injector_mode === "none") return [];
const injectors: ExtensionInjector[] = [];
- if (this.injector.injector_mode === "auto" || this.injector.injector_mode === "discover") {
+ const prefer_launch_injection = this.injector.injector_mode === "auto" && this.launcher.launcher_mode === "local";
+ if (
+ (this.injector.injector_mode === "auto" || this.injector.injector_mode === "discover") &&
+ !prefer_launch_injection
+ ) {
const { DiscoveredExtensionInjector } = await import("../injector/DiscoveredExtensionInjector.js");
injectors.push(new DiscoveredExtensionInjector());
}
@@ -910,6 +942,10 @@ export class ModCDPClient extends ModCDPEventEmitter {
);
injectors.push(new ExtensionsLoadUnpackedInjector());
}
+ if (prefer_launch_injection) {
+ const { DiscoveredExtensionInjector } = await import("../injector/DiscoveredExtensionInjector.js");
+ injectors.push(new DiscoveredExtensionInjector());
+ }
if (this.injector.injector_mode === "auto" || this.injector.injector_mode === "borrow") {
const { BorrowedExtensionInjector } = await import(/* @vite-ignore */ "../injector/BorrowedExtensionInjector.js");
injectors.push(new BorrowedExtensionInjector());
@@ -932,8 +968,6 @@ export class ModCDPClient extends ModCDPEventEmitter {
this.auto_sessions.waitForExecutionContext(session_id, { timeout_ms }),
injector_extension_path: this.injector.injector_extension_path,
injector_extension_id: this.injector.injector_extension_id,
- injector_wake_path: this.injector.injector_wake_path,
- injector_wake_url: this.injector.injector_wake_url,
injector_service_worker_url_includes: this.injector.injector_service_worker_url_includes,
injector_service_worker_url_suffixes: service_worker_url_suffixes,
injector_trust_service_worker_target: trust_service_worker_target,
@@ -1206,15 +1240,17 @@ export class ModCDPClient extends ModCDPEventEmitter {
this.ext_session_id,
);
if (u) {
- this.auto_sessions.recordProtocolEvent(u.event, u.data, u.sessionId);
- this.emit(u.event, this.event_schemas.get(u.event)?.parse(u.data) ?? u.data, u.sessionId);
+ const payload = this._parseEventPayload(u.event, u.data);
+ this.auto_sessions.recordProtocolEvent(u.event, payload as ProtocolPayload, u.sessionId);
+ this.emit(u.event, payload, u.sessionId);
}
return;
}
if (event.method) {
const data = event.params || {};
- this.auto_sessions.recordProtocolEvent(event.method, data, event.sessionId || null);
- this.emit(event.method, this.event_schemas.get(event.method)?.parse(data) ?? data, event.sessionId || null);
+ const payload = this._parseEventPayload(event.method, data);
+ this.auto_sessions.recordProtocolEvent(event.method, payload as ProtocolPayload, event.sessionId || null);
+ this.emit(event.method, payload, event.sessionId || null);
}
}
diff --git a/js/src/index.ts b/js/src/index.ts
index 15026f2..2ea4a62 100644
--- a/js/src/index.ts
+++ b/js/src/index.ts
@@ -9,7 +9,6 @@ export { NoopBrowserLauncher } from "./launcher/NoopBrowserLauncher.js";
export {
DEFAULT_MODCDP_EXTENSION_ID,
DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES,
- DEFAULT_MODCDP_WAKE_PATH,
ExtensionInjector,
defaultModCDPExtensionPath,
} from "./injector/ExtensionInjector.js";
diff --git a/js/src/injector/DiscoveredExtensionInjector.ts b/js/src/injector/DiscoveredExtensionInjector.ts
index 5292899..8d91f55 100644
--- a/js/src/injector/DiscoveredExtensionInjector.ts
+++ b/js/src/injector/DiscoveredExtensionInjector.ts
@@ -13,16 +13,6 @@ export class DiscoveredExtensionInjector extends ExtensionInjector {
);
if (waited) return { ...waited, source: "discovered" };
}
- const woke = await this.wakeConfiguredExtension();
- if (woke) {
- const waited = await this.waitForReadyServiceWorker(
- this.options.injector_service_worker_probe_timeout_ms ?? 10_000,
- {
- matched_only: this.options.injector_trust_service_worker_target,
- },
- );
- if (waited) return { ...waited, source: "discovered" };
- }
if (!this.options.injector_require_service_worker_target) return null;
const waited = await this.waitForReadyServiceWorker(
this.options.injector_service_worker_ready_timeout_ms ?? 60_000,
diff --git a/js/src/injector/ExtensionInjector.ts b/js/src/injector/ExtensionInjector.ts
index 3d1719d..5acf392 100644
--- a/js/src/injector/ExtensionInjector.ts
+++ b/js/src/injector/ExtensionInjector.ts
@@ -1,3 +1,6 @@
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
import type { BrowserLaunchOptions } from "../launcher/BrowserLauncher.js";
import type { UpstreamTransportConfig } from "../transport/UpstreamTransport.js";
import type { ProtocolParams, ProtocolResult } from "../types/modcdp.js";
@@ -7,7 +10,6 @@ import { commands as TargetCommands } from "../types/generated/zod/Target.js";
const EXT_ID_FROM_URL = /^chrome-extension:\/\/([a-z]+)\//;
export const DEFAULT_MODCDP_EXTENSION_ID = "mdedooklbnfejodmnhmkdpkaedafkehf";
export const DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES = ["/modcdp/service_worker.js"];
-export const DEFAULT_MODCDP_WAKE_PATH = "/modcdp/wake.html";
const MODCDP_READY_EXPRESSION =
"Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)";
export const DEFAULT_CDP_SEND_TIMEOUT_MS = 10_000;
@@ -27,8 +29,6 @@ export type ExtensionInjectorConfig = {
waitForExecutionContext?: ((session_id: string, timeout_ms: number) => Promise) | null;
injector_extension_path?: string | null;
injector_extension_id?: string | null;
- injector_wake_path?: string | null;
- injector_wake_url?: string | null;
injector_service_worker_url_includes?: string[];
injector_service_worker_url_suffixes?: string[];
injector_trust_service_worker_target?: boolean;
@@ -42,7 +42,6 @@ export type ExtensionInjectorConfig = {
injector_target_session_poll_interval_ms?: number;
injector_browserbase_api_key?: string | null;
injector_browserbase_base_url?: string | null;
- upstream_reversews_url?: string | null;
upstream_nativemessaging_host_name?: string | null;
upstream_nats_url?: string | null;
upstream_nats_subject_prefix?: string | null;
@@ -58,6 +57,11 @@ export type ExtensionInjectionResult = {
has_debugger?: boolean;
};
+export type PreparedExtension = {
+ unpacked_extension_path: string;
+ cleanup: () => Promise;
+};
+
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
@@ -72,6 +76,69 @@ export function defaultModCDPExtensionPath() {
return "../../../dist/extension.zip";
}
+function firstString(...values: unknown[]) {
+ for (const value of values) {
+ if (typeof value === "string" && value.trim()) return value.trim();
+ }
+ return null;
+}
+
+export async function prepareUnpackedExtension(extension_path: string): Promise {
+ const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-"));
+ const cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true });
+ try {
+ if (extension_path.endsWith(".zip")) {
+ await extractZip(extension_path, unpacked_path);
+ } else {
+ fs.cpSync(extension_path, unpacked_path, { recursive: true });
+ }
+ return { unpacked_extension_path: extensionRoot(unpacked_path), cleanup };
+ } catch (error) {
+ await cleanup();
+ throw error;
+ }
+}
+
+export async function extensionIdFromManifestKey(extension_path: string) {
+ const [crypto, fs, path] = await Promise.all([import("node:crypto"), import("node:fs"), import("node:path")]);
+ const manifest_path = path.join(extension_path, "manifest.json");
+ if (!fs.existsSync(manifest_path)) return null;
+ const manifest = JSON.parse(fs.readFileSync(manifest_path, "utf8")) as Record;
+ const key = firstString(manifest.key);
+ if (!key) return null;
+ const digest = crypto.createHash("sha256").update(Buffer.from(key, "base64")).digest().subarray(0, 16);
+ const alphabet = "abcdefghijklmnop";
+ return [...digest].map((byte) => alphabet[byte >> 4] + alphabet[byte & 0x0f]).join("");
+}
+
+function extensionRoot(unpacked_path: string) {
+ if (fs.existsSync(path.join(unpacked_path, "manifest.json"))) return unpacked_path;
+ const nested_path = path.join(unpacked_path, "extension");
+ if (fs.existsSync(path.join(nested_path, "manifest.json"))) return nested_path;
+ return unpacked_path;
+}
+
+async function extractZip(zip_path: string, destination: string) {
+ const { execFileSync } = await import("node:child_process");
+ const listing = execFileSync("unzip", ["-Z1", zip_path], {
+ encoding: "utf8",
+ });
+ for (const raw_name of listing.split(/\r?\n/)) {
+ if (!raw_name) continue;
+ const name = raw_name.replaceAll("\\", "/");
+ const normalized = path.posix.normalize(name);
+ if (
+ path.posix.isAbsolute(normalized) ||
+ normalized === "." ||
+ normalized === ".." ||
+ normalized.startsWith("../")
+ ) {
+ throw new Error(`zip entry ${JSON.stringify(raw_name)} escapes extension extraction directory`);
+ }
+ }
+ execFileSync("unzip", ["-q", zip_path, "-d", destination]);
+}
+
export class ExtensionInjector {
options: ExtensionInjectorConfig;
protected unusable_target_ids = new Set();
@@ -85,8 +152,6 @@ export class ExtensionInjector {
waitForExecutionContext: null,
injector_extension_path: null,
injector_extension_id: null,
- injector_wake_path: DEFAULT_MODCDP_WAKE_PATH,
- injector_wake_url: null,
injector_service_worker_url_includes: [],
injector_service_worker_url_suffixes: [],
injector_trust_service_worker_target: false,
@@ -100,7 +165,6 @@ export class ExtensionInjector {
injector_target_session_poll_interval_ms: DEFAULT_TARGET_SESSION_POLL_INTERVAL_MS,
injector_browserbase_api_key: null,
injector_browserbase_base_url: null,
- upstream_reversews_url: null,
upstream_nativemessaging_host_name: null,
upstream_nats_url: null,
upstream_nats_subject_prefix: null,
@@ -194,30 +258,6 @@ export class ExtensionInjector {
return TargetCommands["Target.getTargets"].result.parse(await this.send("Target.getTargets")).targetInfos;
}
- protected configuredWakeUrl() {
- if (this.options.injector_wake_url) return this.options.injector_wake_url;
- if (!this.options.injector_extension_id) return null;
- const wake_path = this.options.injector_wake_path ?? DEFAULT_MODCDP_WAKE_PATH;
- if (!wake_path) return null;
- return `chrome-extension://${this.options.injector_extension_id}${wake_path.startsWith("/") ? wake_path : `/${wake_path}`}`;
- }
-
- protected async wakeConfiguredExtension() {
- const wake_url = this.configuredWakeUrl();
- if (!wake_url || typeof this.options.send !== "function") return false;
- try {
- await this.sendWithTimeout("Target.createTarget", {
- url: wake_url,
- background: true,
- hidden: true,
- focus: false,
- });
- return true;
- } catch {
- return false;
- }
- }
-
protected async probeTarget(
target: TargetInfo,
session_timeout_ms = 0,
@@ -283,7 +323,9 @@ export class ExtensionInjector {
) {
const deadline = Date.now() + timeout_ms;
while (Date.now() < deadline) {
- const discovered = await this.discoverReadyServiceWorker({ matched_only });
+ const discovered = await this.discoverReadyServiceWorker({
+ matched_only,
+ });
if (discovered) return discovered;
await delay(this.options.injector_service_worker_poll_interval_ms ?? DEFAULT_SERVICE_WORKER_POLL_INTERVAL_MS);
}
diff --git a/js/src/injector/ExtensionsLoadUnpackedInjector.ts b/js/src/injector/ExtensionsLoadUnpackedInjector.ts
index 4f61a4f..43043c0 100644
--- a/js/src/injector/ExtensionsLoadUnpackedInjector.ts
+++ b/js/src/injector/ExtensionsLoadUnpackedInjector.ts
@@ -1,7 +1,9 @@
-import fs from "node:fs";
-import os from "node:os";
-import path from "node:path";
-import { defaultModCDPExtensionPath, ExtensionInjector, type TargetInfo } from "./ExtensionInjector.js";
+import {
+ defaultModCDPExtensionPath,
+ ExtensionInjector,
+ prepareUnpackedExtension,
+ type TargetInfo,
+} from "./ExtensionInjector.js";
export class ExtensionsLoadUnpackedInjector extends ExtensionInjector {
private unpacked_extension_path: string | null = null;
@@ -13,19 +15,9 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector {
await super.prepare();
return;
}
- if (!extension_path.endsWith(".zip")) {
- const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-"));
- fs.cpSync(extension_path, unpacked_path, { recursive: true });
- this.unpacked_extension_path = extensionRoot(unpacked_path);
- this.cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true });
- await super.prepare();
- return;
- }
- const { execFileSync } = await import("node:child_process");
- const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-"));
- execFileSync("unzip", ["-q", extension_path, "-d", unpacked_path]);
- this.unpacked_extension_path = extensionRoot(unpacked_path);
- this.cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true });
+ const prepared = await prepareUnpackedExtension(extension_path);
+ this.unpacked_extension_path = prepared.unpacked_extension_path;
+ this.cleanup = prepared.cleanup;
await super.prepare();
}
@@ -34,7 +26,9 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector {
if (!extension_path) return null;
let load_result: Record;
try {
- load_result = (await this.send("Extensions.loadUnpacked", { path: extension_path })) as Record;
+ load_result = (await this.send("Extensions.loadUnpacked", {
+ path: extension_path,
+ })) as Record;
} catch (error) {
const load_error = error instanceof Error ? error : new Error(String(error));
if (/Method not available|Method.*not.*found|wasn't found/i.test(load_error.message)) {
@@ -52,7 +46,6 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector {
throw new Error(`Extensions.loadUnpacked returned no extension id (got ${JSON.stringify(load_result)})`);
}
this.options.injector_extension_id = extension_id;
- await this.wakeConfiguredExtension();
const sw_url_prefix = `chrome-extension://${extension_id}/`;
const deadline = Date.now() + (this.options.injector_service_worker_ready_timeout_ms ?? 60_000);
@@ -65,7 +58,12 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector {
const probed = await this.probeTarget(target, this.options.injector_service_worker_probe_timeout_ms, {
allow_attach: true,
});
- if (probed) return { ...probed, source: "extensions_load_unpacked", extension_id };
+ if (probed)
+ return {
+ ...probed,
+ source: "extensions_load_unpacked",
+ extension_id,
+ };
}
await new Promise((resolve) => setTimeout(resolve, this.options.injector_service_worker_poll_interval_ms ?? 100));
}
@@ -78,10 +76,3 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector {
this.cleanup = null;
}
}
-
-function extensionRoot(unpacked_path: string) {
- if (fs.existsSync(path.join(unpacked_path, "manifest.json"))) return unpacked_path;
- const nested_path = path.join(unpacked_path, "extension");
- if (fs.existsSync(path.join(nested_path, "manifest.json"))) return nested_path;
- return unpacked_path;
-}
diff --git a/js/src/injector/LocalBrowserLaunchExtensionInjector.ts b/js/src/injector/LocalBrowserLaunchExtensionInjector.ts
index f407eed..b1ff853 100644
--- a/js/src/injector/LocalBrowserLaunchExtensionInjector.ts
+++ b/js/src/injector/LocalBrowserLaunchExtensionInjector.ts
@@ -1,15 +1,10 @@
-import fs from "node:fs";
-import os from "node:os";
-import path from "node:path";
import type { BrowserLaunchOptions } from "../launcher/BrowserLauncher.js";
-import { defaultModCDPExtensionPath, ExtensionInjector } from "./ExtensionInjector.js";
-
-function firstString(...values: unknown[]) {
- for (const value of values) {
- if (typeof value === "string" && value.trim()) return value.trim();
- }
- return null;
-}
+import {
+ defaultModCDPExtensionPath,
+ extensionIdFromManifestKey,
+ ExtensionInjector,
+ prepareUnpackedExtension,
+} from "./ExtensionInjector.js";
export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector {
private unpacked_extension_path: string | null = null;
@@ -22,20 +17,9 @@ export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector {
await super.prepare();
return;
}
- if (!extension_path.endsWith(".zip")) {
- const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-"));
- fs.cpSync(extension_path, unpacked_path, { recursive: true });
- this.unpacked_extension_path = extensionRoot(unpacked_path);
- this.cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true });
- await this.resolveExtensionId();
- await super.prepare();
- return;
- }
- const { execFileSync } = await import("node:child_process");
- const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-"));
- execFileSync("unzip", ["-q", extension_path, "-d", unpacked_path]);
- this.unpacked_extension_path = extensionRoot(unpacked_path);
- this.cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true });
+ const prepared = await prepareUnpackedExtension(extension_path);
+ this.unpacked_extension_path = prepared.unpacked_extension_path;
+ this.cleanup = prepared.cleanup;
await this.resolveExtensionId();
await super.prepare();
}
@@ -47,8 +31,7 @@ export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector {
}
async inject() {
- const timeout_ms = this.options.injector_service_worker_probe_timeout_ms ?? 10_000;
- const discovered = await this.waitForReadyServiceWorker(timeout_ms, {
+ const discovered = await this.discoverReadyServiceWorker({
matched_only: this.options.injector_trust_service_worker_target,
});
return discovered ? { ...discovered, source: "local_launch" } : null;
@@ -62,7 +45,10 @@ export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector {
private async resolveExtensionId() {
if (this.extension_id) return this.extension_id;
- this.extension_id = firstString(this.options.injector_extension_id);
+ this.extension_id =
+ typeof this.options.injector_extension_id === "string" && this.options.injector_extension_id.trim()
+ ? this.options.injector_extension_id.trim()
+ : null;
if (!this.extension_id && this.unpacked_extension_path) {
this.extension_id = await extensionIdFromManifestKey(this.unpacked_extension_path);
}
@@ -70,22 +56,3 @@ export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector {
return this.extension_id;
}
}
-
-function extensionRoot(unpacked_path: string) {
- if (fs.existsSync(path.join(unpacked_path, "manifest.json"))) return unpacked_path;
- const nested_path = path.join(unpacked_path, "extension");
- if (fs.existsSync(path.join(nested_path, "manifest.json"))) return nested_path;
- return unpacked_path;
-}
-
-async function extensionIdFromManifestKey(extension_path: string) {
- const [crypto, fs, path] = await Promise.all([import("node:crypto"), import("node:fs"), import("node:path")]);
- const manifest_path = path.join(extension_path, "manifest.json");
- if (!fs.existsSync(manifest_path)) return null;
- const manifest = JSON.parse(fs.readFileSync(manifest_path, "utf8")) as Record;
- const key = firstString(manifest.key);
- if (!key) return null;
- const digest = crypto.createHash("sha256").update(Buffer.from(key, "base64")).digest().subarray(0, 16);
- const alphabet = "abcdefghijklmnop";
- return [...digest].map((byte) => alphabet[byte >> 4] + alphabet[byte & 0x0f]).join("");
-}
diff --git a/js/src/launcher/LocalBrowserLauncher.ts b/js/src/launcher/LocalBrowserLauncher.ts
index b1a2708..b31f9ce 100644
--- a/js/src/launcher/LocalBrowserLauncher.ts
+++ b/js/src/launcher/LocalBrowserLauncher.ts
@@ -1,7 +1,7 @@
import { spawn, type ChildProcess } from "node:child_process";
import { once } from "node:events";
import { existsSync, readdirSync, statSync } from "node:fs";
-import { mkdtemp, rm } from "node:fs/promises";
+import { mkdtemp, readFile, rm } from "node:fs/promises";
import net from "node:net";
import type { AddressInfo } from "node:net";
import { homedir, platform, tmpdir } from "node:os";
@@ -121,7 +121,8 @@ function candidatePaths() {
),
]
: ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/opt/google/chrome/chrome"];
- return [process.env.CHROME_PATH, ...chromeForTestingCandidates(), ...canary, ...stock].filter(
+ const chromium = platform() === "linux" ? ["/usr/bin/chromium", "/usr/bin/chromium-browser"] : [];
+ return [process.env.CHROME_PATH, ...chromium, ...canary, ...chromeForTestingCandidates(), ...stock].filter(
(candidate): candidate is string => Boolean(candidate),
);
}
@@ -222,16 +223,70 @@ async function waitForPipeReady(
async function waitForCdpWebSocketUrl(cdp_url: string, timeout_ms: number, poll_interval_ms: number) {
const deadline = Date.now() + timeout_ms;
+ let lastError: unknown = null;
while (Date.now() < deadline) {
try {
return await resolveCdpWebSocketUrl(cdp_url);
- } catch {
+ } catch (error) {
+ lastError = error;
await delay(poll_interval_ms);
}
}
+ if (lastError instanceof Error) {
+ throw new Error(
+ `Chrome at ${cdp_url} did not expose a WebSocket CDP URL within ${timeout_ms}ms: ${lastError.message}`,
+ );
+ }
throw new Error(`Chrome at ${cdp_url} did not expose a WebSocket CDP URL within ${timeout_ms}ms`);
}
+async function readDevToolsActivePort(profile_dir: string) {
+ const activePortPath = path.join(profile_dir, "DevToolsActivePort");
+ let body: string;
+ try {
+ body = await readFile(activePortPath, "utf8");
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
+ throw error;
+ }
+ const [rawPort, websocketPath] = body.trim().split(/\r?\n/);
+ if (!rawPort || !websocketPath) return null;
+ const port = Number(rawPort);
+ if (!Number.isInteger(port) || port <= 0) throw new Error(`Invalid DevToolsActivePort port: ${rawPort}`);
+ return { port, cdp_url: `http://127.0.0.1:${port}`, websocketPath };
+}
+
+async function waitForBrowserSelectedCdpWebSocketUrl(
+ profile_dir: string,
+ timeout_ms: number,
+ poll_interval_ms: number,
+ assertChromeRunning: () => void,
+) {
+ const deadline = Date.now() + timeout_ms;
+ let lastError: unknown = null;
+ while (Date.now() < deadline) {
+ assertChromeRunning();
+ const activePort = await readDevToolsActivePort(profile_dir);
+ if (activePort) {
+ try {
+ return {
+ port: activePort.port,
+ cdp_url: await resolveCdpWebSocketUrl(activePort.cdp_url),
+ };
+ } catch (error) {
+ lastError = error;
+ }
+ }
+ await delay(poll_interval_ms);
+ }
+ if (lastError instanceof Error) {
+ throw new Error(
+ `Chrome did not expose DevToolsActivePort from ${profile_dir} within ${timeout_ms}ms: ${lastError.message}`,
+ );
+ }
+ throw new Error(`Chrome did not expose DevToolsActivePort from ${profile_dir} within ${timeout_ms}ms`);
+}
+
export class LocalBrowserLauncher extends BrowserLauncher {
static findChromeBinary(explicit?: string | null) {
const candidates = [explicit, ...candidatePaths()].filter((candidate): candidate is string => Boolean(candidate));
@@ -260,7 +315,7 @@ export class LocalBrowserLauncher extends BrowserLauncher {
port,
user_data_dir,
headless = process.platform === "linux" && !process.env.DISPLAY,
- sandbox = false,
+ sandbox = process.platform !== "linux",
args = [],
extra_args = [],
remote_debugging = "port",
@@ -272,7 +327,7 @@ export class LocalBrowserLauncher extends BrowserLauncher {
const exe = LocalBrowserLauncher.findChromeBinary(executable_path);
const usePipe = remote_debugging === "pipe";
const useLoopbackCdp = !usePipe || loopback_cdp || port != null;
- const usePort = useLoopbackCdp ? port || (await LocalBrowserLauncher.freePort()) : null;
+ const usePort = useLoopbackCdp ? (port ?? 0) : null;
const profile_dir = user_data_dir || (await mkdtemp(path.join(tmpdir(), "modcdp.")));
const flags = [
...DEFAULT_FLAGS,
@@ -308,6 +363,12 @@ export class LocalBrowserLauncher extends BrowserLauncher {
await terminateProcess(proc);
if (!user_data_dir || cleanup_user_data_dir) await removeProfileDir(profile_dir);
};
+ const assertChromeRunning = () => {
+ if (spawnError) throw spawnError;
+ if (proc.exitCode !== null || proc.signalCode !== null) {
+ throw new Error(`Chrome exited before CDP became ready (exit=${proc.exitCode}, signal=${proc.signalCode}).`);
+ }
+ };
if (usePipe) {
const pipe_write = proc.stdio[3] as NodeJS.WritableStream | null;
@@ -316,24 +377,31 @@ export class LocalBrowserLauncher extends BrowserLauncher {
await close();
throw new Error("Chrome remote-debugging pipe stdio handles were not created.");
}
- if (spawnError) {
- await close();
- throw spawnError;
- }
+ assertChromeRunning();
await waitForPipeReady(pipe_read, pipe_write, chrome_ready_timeout_ms);
- const loopback_cdp_url =
+ const loopback =
usePort == null
? null
- : await waitForCdpWebSocketUrl(
- `http://127.0.0.1:${usePort}`,
- chrome_ready_timeout_ms,
- chrome_ready_poll_interval_ms,
- );
+ : usePort === 0
+ ? await waitForBrowserSelectedCdpWebSocketUrl(
+ profile_dir,
+ chrome_ready_timeout_ms,
+ chrome_ready_poll_interval_ms,
+ assertChromeRunning,
+ )
+ : {
+ port: usePort,
+ cdp_url: await waitForCdpWebSocketUrl(
+ `http://127.0.0.1:${usePort}`,
+ chrome_ready_timeout_ms,
+ chrome_ready_poll_interval_ms,
+ ),
+ };
this.launched = {
proc,
- ...(usePort == null ? {} : { port: usePort }),
+ ...(loopback == null ? {} : { port: loopback.port }),
cdp_url: `pipe://${proc.pid}`,
- ...(loopback_cdp_url == null ? {} : { loopback_cdp_url }),
+ ...(loopback == null ? {} : { loopback_cdp_url: loopback.cdp_url }),
pipe_read,
pipe_write,
profile_dir,
@@ -342,28 +410,32 @@ export class LocalBrowserLauncher extends BrowserLauncher {
return this.launched;
}
- const cdp_port = usePort as number;
- const cdp_url = `http://127.0.0.1:${cdp_port}`;
const deadline = Date.now() + chrome_ready_timeout_ms;
while (Date.now() < deadline) {
- if (spawnError) {
+ try {
+ assertChromeRunning();
+ } catch (error) {
await close();
- throw spawnError;
+ throw error;
}
- if (proc.exitCode !== null || proc.signalCode !== null) {
- await close();
- throw new Error(`Chrome exited before CDP became ready (exit=${proc.exitCode}, signal=${proc.signalCode}).`);
+ const activePort =
+ usePort === 0
+ ? await readDevToolsActivePort(profile_dir)
+ : { port: usePort as number, cdp_url: `http://127.0.0.1:${usePort}` };
+ if (!activePort) {
+ await delay(chrome_ready_poll_interval_ms);
+ continue;
}
try {
- const response = await fetch(`${cdp_url}/json/version`);
+ const response = await fetch(`${activePort.cdp_url}/json/version`);
if (response.ok) {
const version = await response.json();
// cdp_url is resolved from the HTTP discovery endpoint before returning.
this.launched = {
proc,
- port: cdp_port,
- cdp_url: version.webSocketDebuggerUrl ?? cdp_url,
- loopback_cdp_url: version.webSocketDebuggerUrl ?? cdp_url,
+ port: activePort.port,
+ cdp_url: version.webSocketDebuggerUrl ?? activePort.cdp_url,
+ loopback_cdp_url: version.webSocketDebuggerUrl ?? activePort.cdp_url,
profile_dir,
close,
};
@@ -373,6 +445,6 @@ export class LocalBrowserLauncher extends BrowserLauncher {
await new Promise((resolve) => setTimeout(resolve, chrome_ready_poll_interval_ms));
}
await close();
- throw new Error(`Chrome at ${cdp_url} did not become ready within ${chrome_ready_timeout_ms}ms`);
+ throw new Error(`Chrome did not become ready within ${chrome_ready_timeout_ms}ms`);
}
}
diff --git a/js/src/proxy/proxy.ts b/js/src/proxy/proxy.ts
index 3065ae1..46186c7 100644
--- a/js/src/proxy/proxy.ts
+++ b/js/src/proxy/proxy.ts
@@ -1242,8 +1242,6 @@ export function runProxyCli(args = process.argv.slice(2)) {
injector_mode: String(argv["injector-mode"] || "auto") as InjectorOptions["injector_mode"],
injector_extension_path,
injector_extension_id: optionalStringArg(argv, "injector-extension-id"),
- injector_wake_path: optionalStringArg(argv, "injector-wake-path"),
- injector_wake_url: optionalStringArg(argv, "injector-wake-url"),
injector_service_worker_url_includes: optionalStringListArg(argv, "injector-service-worker-url-includes"),
injector_service_worker_url_suffixes: optionalStringListArg(argv, "injector-service-worker-url-suffixes"),
injector_trust_service_worker_target: optionalBooleanArg(argv, "injector-trust-service-worker-target"),
diff --git a/js/src/server/ModCDPServer.ts b/js/src/server/ModCDPServer.ts
index 846bce3..6ceac32 100644
--- a/js/src/server/ModCDPServer.ts
+++ b/js/src/server/ModCDPServer.ts
@@ -7,6 +7,8 @@
// when Chrome refuses Extensions.loadUnpacked.
import type { cdp } from "../types/generated/cdp.js";
+import { commands as nativeCommandSchemas, events as nativeEventSchemas } from "../types/generated/zod.js";
+import { normalizeModCDPPayloadSchema } from "../types/modcdp.js";
import type {
CdpCommandMessage,
CdpEventMessage,
@@ -32,6 +34,13 @@ export const DEFAULT_NATS_BRIDGE_RECONNECT_INTERVAL_MS = 2_000;
export const DEFAULT_NATS_BRIDGE_SUBJECT_PREFIX = "modcdp.default";
type MiddlewarePhase = "request" | "response" | "event";
+type ProtocolCommandSchema = {
+ params: { parse(value: unknown): ProtocolParams };
+ result: { parse(value: unknown): ProtocolResult };
+};
+type ProtocolEventSchema = {
+ parse(value: unknown): ProtocolPayload;
+};
type ModCDPGlobalScope = typeof globalThis &
Record & {
ModCDP?: {
@@ -81,12 +90,38 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
const attachedDebuggees = new Set();
let runtime_types_promise: Promise | null = null;
+ function nativeCommandSchema(method: string) {
+ return (nativeCommandSchemas as Record)[method];
+ }
+
+ function nativeEventSchema(eventName: string) {
+ return (nativeEventSchemas as Record)[eventName];
+ }
+
+ function commandParamsSchema(method: string, command: ModCDPCustomCommandRegistration | null) {
+ return (
+ (command?.params_schema as ProtocolCommandSchema["params"] | null) ?? nativeCommandSchema(method)?.params ?? null
+ );
+ }
+
+ function commandResultSchema(method: string, command: ModCDPCustomCommandRegistration | null) {
+ return (
+ (command?.result_schema as ProtocolCommandSchema["result"] | null) ?? nativeCommandSchema(method)?.result ?? null
+ );
+ }
+
+ function eventPayloadSchema(eventName: string, event: ModCDPCustomEventRegistration | null) {
+ return (event?.event_schema as ProtocolEventSchema | null) ?? nativeEventSchema(eventName) ?? null;
+ }
+
async function publishEvent(eventName: string, payload: ProtocolPayload = {}, cdpSessionId: string | null = null) {
payload = await ModCDPServer.runMiddleware("event", eventName, payload, {
cdpSessionId,
event: { name: eventName, payload },
});
if (payload === undefined) return { event: eventName, emitted: false, reason: "middleware_dropped" };
+ const event = registryMatch(eventBindings, eventName);
+ payload = eventPayloadSchema(eventName, event)?.parse(payload) ?? payload;
for (const listener of eventListeners) {
try {
@@ -122,7 +157,10 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
params: (payload ?? {}) as CdpEventMessage["params"],
};
if (cdpSessionId) message.sessionId = cdpSessionId;
- publishNats(`${nats_bridge_subject_prefix}.browser_to_client`, { type: "modcdp.nats.message", message });
+ publishNats(`${nats_bridge_subject_prefix}.browser_to_client`, {
+ type: "modcdp.nats.message",
+ message,
+ });
emittedThroughNatsBridge = true;
}
@@ -131,13 +169,25 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
if (isCustomEvent) {
const customBinding = globalScope[CUSTOM_EVENT_BINDING_NAME];
if (typeof customBinding === "function") {
- customBinding(encodeBindingPayload({ event: eventName, data: payload, cdpSessionId }));
+ customBinding(
+ encodeBindingPayload({
+ event: eventName,
+ data: payload,
+ cdpSessionId,
+ }),
+ );
emittedThroughBinding = true;
}
} else {
const mirrorBinding = globalScope[UPSTREAM_EVENT_BINDING_NAME];
if (typeof mirrorBinding === "function") {
- mirrorBinding(encodeBindingPayload({ event: eventName, data: payload, cdpSessionId }));
+ mirrorBinding(
+ encodeBindingPayload({
+ event: eventName,
+ data: payload,
+ cdpSessionId,
+ }),
+ );
emittedThroughBinding = true;
}
}
@@ -218,7 +268,12 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
id?: string;
name?: string;
meta?: () =>
- | { cdp_command_name?: unknown; cdp_event_name?: unknown; id?: unknown; name?: unknown }
+ | {
+ cdp_command_name?: unknown;
+ cdp_event_name?: unknown;
+ id?: unknown;
+ name?: unknown;
+ }
| undefined;
}
| string,
@@ -464,7 +519,10 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
reverseBridgeSocket?.readyState === WebSocket.OPEN ||
reverseBridgeSocket?.readyState === WebSocket.CONNECTING
) {
- return { upstream_reversews_url: endpoint, connected: reverseBridgeSocket.readyState === WebSocket.OPEN };
+ return {
+ upstream_reversews_url: endpoint,
+ connected: reverseBridgeSocket.readyState === WebSocket.OPEN,
+ };
}
const ws = new WebSocket(endpoint);
@@ -498,7 +556,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
const chromeApi = globalScope.chrome;
if (!chromeApi?.runtime?.connectNative) {
scheduleNativeBridgeReconnect(nativeBridgeReconnectIntervalMs);
- return { upstream_nativemessaging_host_name: hostName, connected: false, reason: "native_messaging_unavailable" };
+ return {
+ upstream_nativemessaging_host_name: hostName,
+ connected: false,
+ reason: "native_messaging_unavailable",
+ };
}
if (nativeBridgePort) return { upstream_nativemessaging_host_name: hostName, connected: true };
try {
@@ -530,7 +592,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
ModCDPServer.native_bridge_connected = false;
ModCDPServer.native_bridge_last_error = errorMessage(error);
scheduleNativeBridgeReconnect(nativeBridgeReconnectIntervalMs);
- return { upstream_nativemessaging_host_name: hostName, connected: false, reason: errorMessage(error) };
+ return {
+ upstream_nativemessaging_host_name: hostName,
+ connected: false,
+ reason: errorMessage(error),
+ };
}
}
@@ -570,7 +636,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
if (nats_bridge_socket === ws) nats_bridge_socket = null;
scheduleNatsBridgeReconnect(nats_bridge_reconnect_interval_ms);
});
- return { upstream_nats_url: endpoint, upstream_nats_subject_prefix: nats_bridge_subject_prefix, connected: false };
+ return {
+ upstream_nats_url: endpoint,
+ upstream_nats_subject_prefix: nats_bridge_subject_prefix,
+ connected: false,
+ };
}
function writeNats(data: string) {
@@ -614,7 +684,13 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
}
function natsConnectOptions() {
- return { verbose: false, pedantic: false, lang: "modcdp-extension", version: "1", protocol: 1 };
+ return {
+ verbose: false,
+ pedantic: false,
+ lang: "modcdp-extension",
+ version: "1",
+ protocol: 1,
+ };
}
function debuggerSendCommand(
@@ -822,7 +898,12 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
if (!ModCDPServer.loopback_cdp_url) throw new Error(`No loopback_cdp_url configured for ${method}.`);
const ws = await loopbackWS(ModCDPServer.loopback_cdp_url);
const id = nextLoopbackId++;
- const message: { id: number; method: string; params: ProtocolParams; sessionId?: string } = {
+ const message: {
+ id: number;
+ method: string;
+ params: ProtocolParams;
+ sessionId?: string;
+ } = {
id,
method,
params,
@@ -941,7 +1022,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
void connectReverseBridge(endpoint).catch(() => {
scheduleReverseBridgeReconnect(reverseBridgeReconnectIntervalMs);
});
- return { upstream_reversews_url: endpoint, reconnect_interval_ms, connecting: true };
+ return {
+ upstream_reversews_url: endpoint,
+ reconnect_interval_ms,
+ connecting: true,
+ };
},
stopReverseBridge,
startNativeBridge(
@@ -974,7 +1059,12 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
void connectNatsBridge(endpoint).catch(() => {
scheduleNatsBridgeReconnect(nats_bridge_reconnect_interval_ms);
});
- return { upstream_nats_url: endpoint, upstream_nats_subject_prefix, reconnect_interval_ms, connecting: true };
+ return {
+ upstream_nats_url: endpoint,
+ upstream_nats_subject_prefix,
+ reconnect_interval_ms,
+ connecting: true,
+ };
},
ensureOffscreenKeepAlive,
@@ -1010,9 +1100,6 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
upstream_nats_subject_prefix: upstream.upstream_nats_subject_prefix ?? DEFAULT_NATS_BRIDGE_SUBJECT_PREFIX,
});
}
- if (upstream.upstream_mode === "reversews" && upstream.upstream_reversews_url) {
- this.startReverseBridge(upstream.upstream_reversews_url);
- }
if (server_routes) this.routes = { ...defaultRoutes, ...server_routes };
else {
this.routes = { ...defaultRoutes };
@@ -1036,18 +1123,32 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
if (!/^[^.]+\.[^.]+$/.test(name)) throw new Error("name must be in Domain.method form.");
if (typeof handler !== "function" && typeof expression === "string") {
handler = async (params: ProtocolParams = {}, cdpSessionId: string | null = null, method: string = name) => {
- return await evaluateUserExpression({ expression, params, cdpSessionId, method });
+ return await evaluateUserExpression({
+ expression,
+ params,
+ cdpSessionId,
+ method,
+ });
};
}
if (typeof handler !== "function") throw new Error(`Custom command ${name} was registered without a handler.`);
- commandHandlers.set(name, { name, handler, params_schema, result_schema, expression });
+ commandHandlers.set(name, {
+ name,
+ handler,
+ params_schema: normalizeModCDPPayloadSchema(params_schema),
+ result_schema: normalizeModCDPPayloadSchema(result_schema),
+ expression,
+ });
return { name, registered: true };
},
addCustomEvent({ name, event_schema = null }: ModCDPCustomEventRegistration) {
name = normalizeModCDPName(name);
if (!/^[^.]+\.[^.]+$/.test(name)) throw new Error("name must be in Domain.event form.");
- eventBindings.set(name, { name, event_schema });
+ eventBindings.set(name, {
+ name,
+ event_schema: normalizeModCDPPayloadSchema(event_schema),
+ });
return { name, registered: true };
},
@@ -1122,14 +1223,16 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
params = middlewareParams as ProtocolParams;
const command = registryMatch(commandHandlers, method);
+ params = commandParamsSchema(method, command)?.parse(params) ?? params;
let result;
if (command) {
result = await command.handler(params, cdpSessionId, method);
- return this.runMiddleware("response", method, result, {
+ result = await this.runMiddleware("response", method, result, {
cdpSessionId,
request: { ...request, params },
response: { result },
});
+ return commandResultSchema(method, command)?.parse(result) ?? result;
}
let upstream = "chrome_debugger";
@@ -1162,11 +1265,12 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
else if (upstream === "chrome_debugger") result = await this.sendChromeDebugger(method, params);
else throw new Error(`No ModCDP command registered for ${method}.`);
- return this.runMiddleware("response", method, result, {
+ result = await this.runMiddleware("response", method, result, {
cdpSessionId,
request: { ...request, params },
response: { result },
});
+ return commandResultSchema(method, null)?.parse(result) ?? result;
},
attachToSession(cdpSessionId: string | null = null) {
@@ -1188,7 +1292,13 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
async emit(eventName: string, payload: ProtocolPayload = {}, cdpSessionId: string | null = null) {
const event = registryMatch(eventBindings, eventName);
- if (!event) return { event: eventName, emitted: false, reason: "event_not_registered" };
+ if (!event)
+ return {
+ event: eventName,
+ emitted: false,
+ reason: "event_not_registered",
+ };
+ payload = eventPayloadSchema(eventName, event)?.parse(payload) ?? payload;
const customBinding = globalScope[CUSTOM_EVENT_BINDING_NAME];
if (
typeof customBinding !== "function" &&
@@ -1196,18 +1306,30 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
!nativeBridgePort &&
nats_bridge_socket?.readyState !== WebSocket.OPEN
)
- return { event: eventName, emitted: false, reason: "binding_not_installed" };
+ return {
+ event: eventName,
+ emitted: false,
+ reason: "binding_not_installed",
+ };
return publishEvent(eventName, payload, cdpSessionId);
},
- async discoverLoopbackCDP(): Promise<{ loopback_cdp_url: string | null; verified: boolean; version?: unknown }> {
+ async discoverLoopbackCDP(): Promise<{
+ loopback_cdp_url: string | null;
+ verified: boolean;
+ version?: unknown;
+ }> {
if (!this.browser_token) return { loopback_cdp_url: null as null, verified: false };
const url = "http://127.0.0.1:9222";
const previousLoopbackUrl = this.loopback_cdp_url;
const fail = (version?: unknown) => {
this.loopback_cdp_url = previousLoopbackUrl ?? null;
- return { loopback_cdp_url: null as null, verified: false, ...(version ? { version } : {}) };
+ return {
+ loopback_cdp_url: null as null,
+ verified: false,
+ ...(version ? { version } : {}),
+ };
};
try {
const version = await fetch(`${url}/json/version`).then((response) => response.ok && response.json());
@@ -1242,7 +1364,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
if (result.result?.value !== true) return fail(version);
await initializeLoopbackCDP();
- return { loopback_cdp_url: this.loopback_cdp_url, verified: true, version };
+ return {
+ loopback_cdp_url: this.loopback_cdp_url,
+ verified: true,
+ version,
+ };
} catch {
return fail();
}
@@ -1273,7 +1399,10 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
let resolvedTabUrl: string | null = null;
if (!resolvedTabId) {
const [tab] = chromeApi.tabs?.query
- ? await chromeApi.tabs.query({ active: true, lastFocusedWindow: true })
+ ? await chromeApi.tabs.query({
+ active: true,
+ lastFocusedWindow: true,
+ })
: [];
resolvedTabId = tab?.id || null;
resolvedTabUrl = tab?.url || tab?.pendingUrl || null;
@@ -1331,13 +1460,22 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis
const resolvedDebuggee = debuggee ?? compactDebuggee({ tabId, targetId, extensionId });
if (Object.keys(resolvedDebuggee).length === 0) {
let tab: chrome.tabs.Tab | undefined;
- [tab] = await chromeApi.tabs.query({ active: true, lastFocusedWindow: true });
+ [tab] = await chromeApi.tabs.query({
+ active: true,
+ lastFocusedWindow: true,
+ });
if (!tab?.id) [tab] = await chromeApi.tabs.query({});
if (!tab?.id) {
try {
- tab = await chromeApi.tabs.create({ url: "https://example.com/#modcdp", active: true });
+ tab = await chromeApi.tabs.create({
+ url: "https://example.com/#modcdp",
+ active: true,
+ });
} catch {
- const win = await chromeApi.windows.create({ url: "https://example.com/#modcdp", focused: true });
+ const win = await chromeApi.windows.create({
+ url: "https://example.com/#modcdp",
+ focused: true,
+ });
tab = win?.tabs?.[0];
}
}
diff --git a/js/src/translate/translate.ts b/js/src/translate/translate.ts
index 2338f2d..da94e2a 100644
--- a/js/src/translate/translate.ts
+++ b/js/src/translate/translate.ts
@@ -173,9 +173,11 @@ export function wrapCustomCommand(
method: string,
params: ProtocolParams = {},
cdpSessionId: string | null = null,
-): cdp.types.ts.Runtime.EvaluateParams {
+): cdp.types.ts.Runtime.CallFunctionOnParams {
return {
- functionDeclaration: `async function() { return JSON.stringify(await globalThis.ModCDP.handleCommand(${JSON.stringify(method)}, ${JSON.stringify(params)}, ${JSON.stringify(cdpSessionId)})); }`,
+ functionDeclaration:
+ "async function(method, paramsJson, cdpSessionId) { return JSON.stringify(await globalThis.ModCDP.handleCommand(method, JSON.parse(paramsJson), cdpSessionId)); }",
+ arguments: [{ value: method }, { value: JSON.stringify(params) }, { value: cdpSessionId }],
awaitPromise: true,
returnByValue: true,
};
diff --git a/js/src/transport/ReverseWebSocketUpstreamTransport.ts b/js/src/transport/ReverseWebSocketUpstreamTransport.ts
index 6c2b379..1425a52 100644
--- a/js/src/transport/ReverseWebSocketUpstreamTransport.ts
+++ b/js/src/transport/ReverseWebSocketUpstreamTransport.ts
@@ -51,7 +51,7 @@ export class ReverseWebSocketUpstreamTransport extends UpstreamTransport {
}
getInjectorConfig() {
- return { upstream_reversews_url: this.url };
+ return {};
}
private setBind(bind: string) {
diff --git a/js/src/types/modcdp.ts b/js/src/types/modcdp.ts
index 5cb398a..b5602dd 100644
--- a/js/src/types/modcdp.ts
+++ b/js/src/types/modcdp.ts
@@ -153,7 +153,6 @@ export const ModCDPUpstreamOptionsSchema = z
upstream_nats_subject_prefix: z.string().nullable().optional(),
upstream_nats_wait_timeout_ms: z.number().positive().optional(),
upstream_reversews_bind: z.string().nullable().optional(),
- upstream_reversews_url: z.string().nullable().optional(),
upstream_reversews_wait_timeout_ms: z.number().positive().optional(),
upstream_nativemessaging_manifest: z.string().nullable().optional(),
upstream_nativemessaging_manifests: z.array(z.string()).nullable().optional(),
diff --git a/js/test/ModCDPClient_protocol_validation.test.ts b/js/test/ModCDPClient_protocol_validation.test.ts
new file mode 100644
index 0000000..8d82d43
--- /dev/null
+++ b/js/test/ModCDPClient_protocol_validation.test.ts
@@ -0,0 +1,113 @@
+import assert from "node:assert/strict";
+import { test } from "vitest";
+import { z } from "zod";
+
+import { ModCDPClient, type ModCDPClientInstance } from "../src/client/ModCDPClient.js";
+import type { cdp } from "../src/types/generated/cdp.js";
+
+test("protocol validation covers native methods, native events, custom methods, custom events, and native overrides", async () => {
+ type CustomClient = ModCDPClientInstance<
+ {
+ "Custom.echo": { params: { text: string }; result: { ok: boolean } };
+ "Target.getTargets": {
+ params: cdp.types.ts.Target.GetTargetsParams;
+ result: cdp.types.ts.Target.GetTargetsResult & {
+ targetInfos: Array;
+ };
+ };
+ },
+ { "Custom.ready": { ok: boolean } }
+ >;
+
+ const client = new ModCDPClient() as CustomClient;
+
+ const runtimeParams: cdp.types.ts.Runtime.EvaluateParams = { expression: "1 + 1", returnByValue: true };
+ const runtimeResult: cdp.types.ts.Runtime.EvaluateResult = {
+ result: { type: "number", value: 2, description: "2" },
+ };
+ const nativeEvent: cdp.types.ts.Target.TargetCreatedEvent = {
+ targetInfo: {
+ targetId: "target-1",
+ type: "page",
+ title: "Example",
+ url: "https://example.com",
+ attached: false,
+ canAccessOpener: false,
+ },
+ };
+ const customParams: Parameters[0] = { text: "ok" };
+ const customResult: Awaited> = { ok: true };
+
+ if (false) {
+ // @ts-expect-error Runtime.evaluate params require expression.
+ const badRuntimeParams: cdp.types.ts.Runtime.EvaluateParams = {};
+ void badRuntimeParams;
+ const badNativeEvent: cdp.types.ts.Target.TargetCreatedEvent = {
+ targetInfo: {
+ ...nativeEvent.targetInfo,
+ // @ts-expect-error Target.targetCreated targetInfo.targetId is a string.
+ targetId: 1,
+ },
+ };
+ void badNativeEvent;
+ // @ts-expect-error Custom.echo requires text.
+ client.Custom.echo({ id: "wrong" });
+ // @ts-expect-error Custom.echo returns ok as boolean.
+ const badCustomResult: Awaited> = { ok: "yes" };
+ void badCustomResult;
+ await client.Mod.addMiddleware({
+ name: client.Target.getTargets,
+ phase: client.RESPONSE,
+ expression: "async (value, next) => next(value)",
+ });
+ }
+
+ assert.deepEqual(client.command_params_schemas.get("Runtime.evaluate")?.parse(runtimeParams), runtimeParams);
+ assert.deepEqual(client.command_result_schemas.get("Runtime.evaluate")?.parse(runtimeResult), runtimeResult);
+ assert.deepEqual(client.event_schemas.get("Target.targetCreated")?.parse(nativeEvent), nativeEvent);
+ assert.throws(() => client.command_params_schemas.get("Runtime.evaluate")?.parse({}));
+ assert.throws(() => client.command_result_schemas.get("Runtime.evaluate")?.parse({}));
+ assert.throws(() => client.event_schemas.get("Target.targetCreated")?.parse({}));
+
+ await client.Mod.addCustomCommand("Custom.echo", {
+ params_schema: z.object({ text: z.string() }),
+ result_schema: z.object({ ok: z.boolean() }),
+ });
+ await client.Mod.addCustomEvent("Custom.ready", { event_schema: z.object({ ok: z.boolean() }) });
+
+ assert.deepEqual(client.command_params_schemas.get("Custom.echo")?.parse(customParams), customParams);
+ assert.deepEqual(client.command_result_schemas.get("Custom.echo")?.parse(customResult), customResult);
+ assert.deepEqual(client.event_schemas.get("Custom.ready")?.parse({ ok: true }), { ok: true });
+ assert.throws(() => client.command_params_schemas.get("Custom.echo")?.parse({ text: 1 }));
+ assert.throws(() => client.command_result_schemas.get("Custom.echo")?.parse({ ok: "yes" }));
+ assert.throws(() => client.event_schemas.get("Custom.ready")?.parse({ ok: "yes" }));
+
+ await client.Mod.addCustomCommand({
+ name: client.Target.getTargets,
+ result_schema: z.object({
+ targetInfos: z.array(
+ z.object({
+ targetId: z.string(),
+ type: z.string(),
+ title: z.string(),
+ url: z.string(),
+ attached: z.boolean(),
+ canAccessOpener: z.boolean(),
+ tabId: z.number().optional(),
+ }),
+ ),
+ }),
+ });
+ await client.Mod.addCustomEvent({ name: client.Target.targetCreated });
+
+ const extendedTargets: Awaited> = {
+ targetInfos: [{ ...nativeEvent.targetInfo, tabId: 7 }],
+ };
+ assert.deepEqual(client.command_result_schemas.get("Target.getTargets")?.parse(extendedTargets), extendedTargets);
+ assert.deepEqual(
+ client.event_schemas.get("Target.targetCreated")?.parse({ targetInfo: { ...nativeEvent.targetInfo, tabId: 7 } }),
+ {
+ targetInfo: { ...nativeEvent.targetInfo, tabId: 7 },
+ },
+ );
+});
diff --git a/js/test/test.AutoSessionRouter.ts b/js/test/test.AutoSessionRouter.ts
index 42921be..b5e7414 100644
--- a/js/test/test.AutoSessionRouter.ts
+++ b/js/test/test.AutoSessionRouter.ts
@@ -66,7 +66,6 @@ test("AutoSessionRouter bounds detached session guards and clears them when a se
test("AutoSessionRouter tracks real target sessions and execution contexts", async () => {
const chrome = await new LocalBrowserLauncher({
headless: true,
- sandbox: process.platform !== "linux",
}).launch();
const ws = new WebSocket(chrome.cdp_url!);
await once(ws, "open");
@@ -75,7 +74,7 @@ test("AutoSessionRouter tracks real target sessions and execution contexts", asy
const router = new AutoSessionRouter(
(method, params = {}, session_id = null) =>
send(method, params as Record, session_id) as Promise>,
- () => 5_000,
+ () => 30_000,
);
function send(method: string, params: Record = {}, session_id: string | null = null) {
@@ -126,7 +125,7 @@ test("AutoSessionRouter tracks real target sessions and execution contexts", asy
const session_id = router.sessionIdForTarget(target_id)!;
const context_promise = router.waitForExecutionContext(session_id, {
- timeout_ms: 5_000,
+ timeout_ms: 30_000,
});
await send("Runtime.enable", {}, session_id);
await expect(context_promise).resolves.toEqual(expect.any(Number));
diff --git a/js/test/test.BorrowedExtensionInjector.ts b/js/test/test.BorrowedExtensionInjector.ts
index 00291b2..c4865b3 100644
--- a/js/test/test.BorrowedExtensionInjector.ts
+++ b/js/test/test.BorrowedExtensionInjector.ts
@@ -3,21 +3,28 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "vitest";
-import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js";
import { ModCDPClient } from "../src/client/ModCDPClient.js";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
test("BorrowedExtensionInjector bootstraps ModCDP inside a live extension service worker", async () => {
- const chrome = await new LocalBrowserLauncher({
- headless: true,
- sandbox: process.platform !== "linux",
- extra_args: [`--load-extension=${EXTENSION_PATH}`],
- }).launch();
+ const owner = new ModCDPClient({
+ launcher: {
+ launcher_mode: "local",
+ launcher_options: { headless: true },
+ },
+ upstream: { upstream_mode: "ws" },
+ injector: {
+ injector_mode: "auto",
+ injector_extension_path: EXTENSION_PATH,
+ injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
+ injector_trust_service_worker_target: true,
+ },
+ });
const cdp = new ModCDPClient({
launcher: { launcher_mode: "remote" },
- upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url },
+ upstream: { upstream_mode: "ws" },
injector: {
injector_mode: "borrow",
injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
@@ -26,13 +33,17 @@ test("BorrowedExtensionInjector bootstraps ModCDP inside a live extension servic
});
try {
+ await owner.connect();
+ cdp.upstream.upstream_cdp_url = owner.cdp_url;
await cdp.connect();
assert.equal(cdp.connect_timing?.injector_source, "borrowed");
assert.equal(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf");
- const target_infos = ((await cdp.send("Target.getTargets")) as { targetInfos?: unknown[] }).targetInfos ?? [];
- assert.ok(target_infos.length > 0);
+ assert.equal(
+ await cdp.Mod.evaluate({ expression: "chrome.runtime.getURL('modcdp/service_worker.js')" }),
+ "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js",
+ );
} finally {
await cdp.close();
- await chrome.close();
+ await owner.close();
}
}, 60_000);
diff --git a/js/test/test.DiscoveredExtensionInjector.ts b/js/test/test.DiscoveredExtensionInjector.ts
index c47d861..9c864e2 100644
--- a/js/test/test.DiscoveredExtensionInjector.ts
+++ b/js/test/test.DiscoveredExtensionInjector.ts
@@ -1,105 +1,49 @@
import assert from "node:assert/strict";
-import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
-import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "vitest";
-import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js";
import { ModCDPClient } from "../src/client/ModCDPClient.js";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
-const CUSTOM_EXTENSION_ID = "hhklgmbgnbeghnjidampacgmgnhelifg";
-const CUSTOM_EXTENSION_PUBLIC_KEY =
- "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzG1LUbtH0aHMKjTAUeT0saY8xfnRNENctFJme3C1qnsqT7PAXMxJC4nT7tBZy2gEGRirBb3zIZ3OyAu9a0QR8lTLupDp4qHWOhQ7dl9ZjxjQdYa4Gby0xuXLdQrJIxDbmuv+UVJvYa8vRTwQB8koygbzDDDP5/YiB6mc0hbh8XBb82Ossy7T280k8280o/rS0CXdioUraCHj58PDhfxbs18TBcYfOjuRqua9J2oddxobtGehSD0gDtbvn2IWDtRajOlgZZyuS1vLoSR7C1ulFzpRSYPEMhI2x+wphut7E3QImyJ577YeULVGpt988FcixOou7udjx3/IUWjpq8046wIDAQAB";
test("DiscoveredExtensionInjector attaches to an already-loaded real ModCDP extension", async () => {
- const chrome = await new LocalBrowserLauncher({
- headless: true,
- sandbox: process.platform !== "linux",
- extra_args: [`--load-extension=${EXTENSION_PATH}`],
- }).launch();
- const cdp = new ModCDPClient({
- launcher: { launcher_mode: "remote" },
- upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url },
+ const owner = new ModCDPClient({
+ launcher: {
+ launcher_mode: "local",
+ launcher_options: { headless: true },
+ },
+ upstream: { upstream_mode: "ws" },
injector: {
- injector_mode: "discover",
+ injector_mode: "auto",
+ injector_extension_path: EXTENSION_PATH,
injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
injector_trust_service_worker_target: true,
},
});
-
- try {
- await cdp.connect();
- assert.equal(cdp.connect_timing?.injector_source, "discovered");
- assert.equal(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf");
- const service_worker_url = await cdp.Mod.evaluate({
- expression: "chrome.runtime.getURL('modcdp/service_worker.js')",
- });
- assert.match(
- String(service_worker_url),
- /^chrome-extension:\/\/mdedooklbnfejodmnhmkdpkaedafkehf\/modcdp\/service_worker\.js$/,
- );
- } finally {
- await cdp.close();
- await chrome.close();
- }
-}, 60_000);
-
-test("DiscoveredExtensionInjector selects the configured extension when multiple ModCDP workers exist", async () => {
- const custom_extension_path = await mkdtemp(path.join(tmpdir(), "modcdp-custom-extension-"));
- await cp(EXTENSION_PATH, custom_extension_path, { recursive: true });
- const manifest_path = path.join(custom_extension_path, "manifest.json");
- const manifest = JSON.parse(await readFile(manifest_path, "utf8")) as Record;
- manifest.key = CUSTOM_EXTENSION_PUBLIC_KEY;
- manifest.name = "ModCDP Bridge Custom Test";
- await writeFile(manifest_path, `${JSON.stringify(manifest, null, 2)}\n`);
-
- const chrome = await new LocalBrowserLauncher({
- headless: true,
- sandbox: process.platform !== "linux",
- extra_args: [`--load-extension=${EXTENSION_PATH},${custom_extension_path}`],
- }).launch();
const cdp = new ModCDPClient({
launcher: { launcher_mode: "remote" },
- upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url },
+ upstream: { upstream_mode: "ws" },
injector: {
injector_mode: "discover",
- injector_extension_id: CUSTOM_EXTENSION_ID,
injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
injector_trust_service_worker_target: true,
- injector_require_service_worker_target: true,
},
});
try {
+ await owner.connect();
+ cdp.upstream.upstream_cdp_url = owner.cdp_url;
await cdp.connect();
assert.equal(cdp.connect_timing?.injector_source, "discovered");
- assert.equal(cdp.extension_id, CUSTOM_EXTENSION_ID);
- assert.equal(await cdp.Mod.evaluate({ expression: "chrome.runtime.id" }), CUSTOM_EXTENSION_ID);
-
- const targets = (await cdp.sendRaw("Target.getTargets")) as {
- targetInfos: { type?: string; url?: string }[];
- };
- const modcdp_workers = targets.targetInfos.filter(
- (target) => target.type === "service_worker" && target.url?.endsWith("/modcdp/service_worker.js"),
- );
- assert.equal(
- modcdp_workers.some(
- (target) => target.url === `chrome-extension://${CUSTOM_EXTENSION_ID}/modcdp/service_worker.js`,
- ),
- true,
- );
+ assert.equal(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf");
assert.equal(
- modcdp_workers.some(
- (target) => target.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js",
- ),
- true,
+ await cdp.Mod.evaluate({ expression: "chrome.runtime.getURL('modcdp/service_worker.js')" }),
+ "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js",
);
} finally {
await cdp.close();
- await chrome.close();
- await rm(custom_extension_path, { recursive: true, force: true });
+ await owner.close();
}
}, 60_000);
diff --git a/js/test/test.ExtensionInjector.ts b/js/test/test.ExtensionInjector.ts
index ad22de7..85f3bb8 100644
--- a/js/test/test.ExtensionInjector.ts
+++ b/js/test/test.ExtensionInjector.ts
@@ -1,148 +1,14 @@
import assert from "node:assert/strict";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
import { test } from "vitest";
-import { ExtensionInjector, type ExtensionInjectionResult } from "../src/injector/ExtensionInjector.js";
-import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js";
-import { CdpSocket } from "./helpers.BrowserLauncher.js";
-
-const HERE = path.dirname(fileURLToPath(import.meta.url));
-const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
+import { ExtensionInjector } from "../src/injector/ExtensionInjector.js";
class ProbeExtensionInjector extends ExtensionInjector {
- async inject(): Promise {
- return await this.waitForReadyServiceWorker(this.options.injector_service_worker_ready_timeout_ms ?? 60_000, {
- matched_only: true,
- });
- }
-
matches(target: { type?: string; url?: string }) {
return this.serviceWorkerTargetMatches(target);
}
-
- async sendTimed(method: string, params: Record, session_id: string | null, timeout_ms: number) {
- return await this.sendWithTimeout(method, params, session_id, timeout_ms);
- }
-
- async wake() {
- return await this.wakeConfiguredExtension();
- }
}
-test("ExtensionInjector probes a real extension service worker with shared base config", async () => {
- const chrome = await new LocalBrowserLauncher({
- headless: true,
- sandbox: process.platform !== "linux",
- extra_args: [`--load-extension=${EXTENSION_PATH}`],
- }).launch();
- const cdp = await CdpSocket.connect(chrome.cdp_url!);
- const injector = new ProbeExtensionInjector({
- send: (method, params = {}, session_id = null) =>
- cdp.send(method, params as Record, session_id ?? undefined),
- attachToTarget: async (target_id) => {
- const attached = await cdp.send("Target.attachToTarget", { targetId: target_id, flatten: true });
- return typeof attached.sessionId === "string" ? attached.sessionId : null;
- },
- injector_extension_id: "mdedooklbnfejodmnhmkdpkaedafkehf",
- injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
- injector_trust_service_worker_target: true,
- });
-
- try {
- assert.deepEqual(injector.getLauncherConfig(), {});
- assert.deepEqual(injector.getTransportConfig(), { injector_extension_id: "mdedooklbnfejodmnhmkdpkaedafkehf" });
- const result = await injector.inject();
- assert.equal(result?.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf");
- assert.equal(result?.url?.endsWith("/modcdp/service_worker.js"), true);
- } finally {
- await cdp.close();
- await injector.close();
- await chrome.close();
- }
-}, 60_000);
-
-test("ExtensionInjector keeps the ModCDP service worker alive through offscreen keepalive", async () => {
- const chrome = await new LocalBrowserLauncher({
- headless: true,
- sandbox: process.platform !== "linux",
- extra_args: [`--load-extension=${EXTENSION_PATH}`],
- }).launch();
- const cdp = await CdpSocket.connect(chrome.cdp_url!);
- const injector = new ProbeExtensionInjector({
- send: (method, params = {}, session_id = null) =>
- cdp.send(method, params as Record, session_id ?? undefined),
- attachToTarget: async (target_id) => {
- const attached = await cdp.send("Target.attachToTarget", { targetId: target_id, flatten: true });
- return typeof attached.sessionId === "string" ? attached.sessionId : null;
- },
- injector_extension_id: "mdedooklbnfejodmnhmkdpkaedafkehf",
- injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
- injector_trust_service_worker_target: true,
- });
-
- try {
- const result = await injector.inject();
- assert.equal(result?.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf");
- const session_id = result?.session_id;
- assert.equal(typeof session_id, "string");
-
- let contexts: { type?: string; url?: string }[] = [];
- for (let attempt = 0; attempt < 50; attempt++) {
- const evaluated = await cdp.send(
- "Runtime.evaluate",
- {
- expression:
- "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => ({ type: context.contextType, url: context.documentUrl || context.origin || '' })))",
- awaitPromise: true,
- returnByValue: true,
- },
- session_id,
- );
- contexts = (evaluated.result as { value?: { type?: string; url?: string }[] }).value ?? [];
- if (
- contexts.some(
- (context) =>
- context.type === "OFFSCREEN_DOCUMENT" &&
- context.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html",
- )
- ) {
- break;
- }
- await new Promise((resolve) => setTimeout(resolve, 100));
- }
- assert.equal(
- contexts.some(
- (context) =>
- context.type === "OFFSCREEN_DOCUMENT" &&
- context.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html",
- ),
- true,
- );
-
- await new Promise((resolve) => setTimeout(resolve, 3_000));
- const targets = (await cdp.send("Target.getTargets")) as { targetInfos?: { type?: string; url?: string }[] };
- assert.equal(
- targets.targetInfos?.some(
- (target) =>
- target.type === "service_worker" &&
- target.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js",
- ),
- true,
- );
- const version = await cdp.send(
- "Runtime.evaluate",
- { expression: "globalThis.ModCDP?.__ModCDPServerVersion", returnByValue: true },
- session_id,
- );
- assert.equal((version.result as { value?: unknown }).value, 2);
- } finally {
- await cdp.close();
- await injector.close();
- await chrome.close();
- }
-}, 60_000);
-
test("ExtensionInjector owns shared injector config", async () => {
const injector = new ProbeExtensionInjector({
injector_extension_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
@@ -171,71 +37,6 @@ test("ExtensionInjector owns shared injector config", async () => {
}
});
-test("ExtensionInjector sendWithTimeout enforces cdp send timeout", async () => {
- const chrome = await new LocalBrowserLauncher({
- headless: true,
- sandbox: process.platform !== "linux",
- }).launch();
- const cdp = await CdpSocket.connect(chrome.cdp_url!);
- let target_id: string | null = null;
- const injector = new ProbeExtensionInjector({
- send: (method, params = {}, session_id = null) =>
- cdp.send(method, params as Record, session_id ?? undefined),
- });
-
- try {
- const created = await cdp.send("Target.createTarget", { url: "about:blank#modcdp-timeout" });
- target_id = created.targetId as string;
- const attached = await cdp.send("Target.attachToTarget", { targetId: target_id, flatten: true });
- const session_id = attached.sessionId as string;
- await cdp.send("Runtime.enable", {}, session_id);
- await assert.rejects(
- () =>
- injector.sendTimed(
- "Runtime.evaluate",
- { expression: "new Promise(() => {})", awaitPromise: true },
- session_id,
- 5,
- ),
- /Runtime\.evaluate timed out after 5ms/,
- );
- } finally {
- if (target_id) await cdp.send("Target.closeTarget", { targetId: target_id }).catch(() => ({}));
- await injector.close();
- await cdp.close();
- await chrome.close();
- }
-});
-
-test("ExtensionInjector wakes configured extension with a hidden background target", async () => {
- const chrome = await new LocalBrowserLauncher({
- headless: true,
- sandbox: process.platform !== "linux",
- extra_args: [`--load-extension=${EXTENSION_PATH}`],
- }).launch();
- const cdp = await CdpSocket.connect(chrome.cdp_url!);
- const injector = new ProbeExtensionInjector({
- injector_extension_id: "mdedooklbnfejodmnhmkdpkaedafkehf",
- send: (method, params = {}, session_id = null) =>
- cdp.send(method, params as Record, session_id ?? undefined),
- });
-
- try {
- assert.equal(await injector.wake(), true);
- const targets = (await cdp.send("Target.getTargets")) as { targetInfos?: { url?: string }[] };
- assert.equal(
- targets.targetInfos?.some(
- (target) => target.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/wake.html",
- ),
- true,
- );
- } finally {
- await injector.close();
- await cdp.close();
- await chrome.close();
- }
-});
-
test("ExtensionInjector base inject reports the subclass name", async () => {
await assert.rejects(() => new ExtensionInjector().inject(), /ExtensionInjector\.inject is not implemented/);
});
diff --git a/js/test/test.ExtensionsLoadUnpackedInjector.ts b/js/test/test.ExtensionsLoadUnpackedInjector.ts
index a514045..65f3c2f 100644
--- a/js/test/test.ExtensionsLoadUnpackedInjector.ts
+++ b/js/test/test.ExtensionsLoadUnpackedInjector.ts
@@ -1,29 +1,21 @@
import assert from "node:assert/strict";
+import { existsSync } from "node:fs";
+import path from "node:path";
import { test } from "vitest";
import { ExtensionsLoadUnpackedInjector } from "../src/injector/ExtensionsLoadUnpackedInjector.js";
-import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js";
-import { CdpSocket } from "./helpers.BrowserLauncher.js";
-test("ExtensionsLoadUnpackedInjector exercises the real CDP loadUnpacked path", async () => {
- const chrome = await new LocalBrowserLauncher({
- headless: true,
- sandbox: process.platform !== "linux",
- }).launch();
- const cdp = await CdpSocket.connect(chrome.cdp_url!);
- const injector = new ExtensionsLoadUnpackedInjector({
- send: (method, params = {}, session_id = null) =>
- cdp.send(method, params as Record, session_id ?? undefined),
- });
+test("ExtensionsLoadUnpackedInjector prepares the default packaged extension zip", async () => {
+ const injector = new ExtensionsLoadUnpackedInjector();
try {
await injector.prepare();
- const result = await injector.inject();
- assert.equal(result, null);
- assert.match(injector.last_error?.message ?? "", /Method not available|Method.*not.*found|wasn't found/i);
+ const unpacked_extension_path = (injector as unknown as { unpacked_extension_path?: string | null })
+ .unpacked_extension_path;
+ assert.equal(typeof unpacked_extension_path, "string");
+ assert.match(unpacked_extension_path!, /modcdp-extension-/);
+ assert.equal(existsSync(path.join(unpacked_extension_path!, "manifest.json")), true);
} finally {
- await cdp.close();
await injector.close();
- await chrome.close();
}
-}, 60_000);
+});
diff --git a/js/test/test.LocalBrowserLaunchExtensionInjector.ts b/js/test/test.LocalBrowserLaunchExtensionInjector.ts
index d19d448..70800ba 100644
--- a/js/test/test.LocalBrowserLaunchExtensionInjector.ts
+++ b/js/test/test.LocalBrowserLaunchExtensionInjector.ts
@@ -1,77 +1,131 @@
import assert from "node:assert/strict";
+import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "vitest";
import { DEFAULT_MODCDP_EXTENSION_ID } from "../src/injector/ExtensionInjector.js";
import { LocalBrowserLaunchExtensionInjector } from "../src/injector/LocalBrowserLaunchExtensionInjector.js";
-import { ModCDPClient } from "../src/client/ModCDPClient.js";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
-test("LocalBrowserLaunchExtensionInjector loads the real extension during local launch", async () => {
- const cdp = new ModCDPClient({
- launcher: {
- launcher_mode: "local",
- launcher_options: {
- headless: process.platform === "linux" && !process.env.DISPLAY,
- sandbox: process.platform !== "linux",
- },
- },
- upstream: { upstream_mode: "ws" },
- injector: {
- injector_mode: "inject",
- injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
- injector_trust_service_worker_target: true,
- injector_service_worker_probe_timeout_ms: 30_000,
- },
- client: {
- client_cdp_send_timeout_ms: 30_000,
- },
+function crc32(data: Buffer) {
+ let crc = 0xffffffff;
+ for (const byte of data) {
+ crc ^= byte;
+ for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
+ }
+ return (crc ^ 0xffffffff) >>> 0;
+}
+
+function storedZipEntry(name: string, data: Buffer) {
+ const name_buffer = Buffer.from(name);
+ const checksum = crc32(data);
+ const local = Buffer.alloc(30);
+ local.writeUInt32LE(0x04034b50, 0);
+ local.writeUInt16LE(20, 4);
+ local.writeUInt32LE(checksum, 14);
+ local.writeUInt32LE(data.length, 18);
+ local.writeUInt32LE(data.length, 22);
+ local.writeUInt16LE(name_buffer.length, 26);
+ const central = Buffer.alloc(46);
+ central.writeUInt32LE(0x02014b50, 0);
+ central.writeUInt16LE(20, 4);
+ central.writeUInt16LE(20, 6);
+ central.writeUInt32LE(checksum, 16);
+ central.writeUInt32LE(data.length, 20);
+ central.writeUInt32LE(data.length, 24);
+ central.writeUInt16LE(name_buffer.length, 28);
+ const central_offset = local.length + name_buffer.length + data.length;
+ const end = Buffer.alloc(22);
+ end.writeUInt32LE(0x06054b50, 0);
+ end.writeUInt16LE(1, 8);
+ end.writeUInt16LE(1, 10);
+ end.writeUInt32LE(central.length + name_buffer.length, 12);
+ end.writeUInt32LE(central_offset, 16);
+ return Buffer.concat([local, name_buffer, data, central, name_buffer, end]);
+}
+
+test("LocalBrowserLaunchExtensionInjector rejects zip entries outside extraction directory", async () => {
+ const temp_dir = mkdtempSync(path.join(os.tmpdir(), "modcdp-bad-zip-"));
+ const zip_path = path.join(temp_dir, "extension.zip");
+ writeFileSync(zip_path, storedZipEntry("../evil.txt", Buffer.from("evil")));
+ const injector = new LocalBrowserLaunchExtensionInjector({
+ injector_extension_path: zip_path,
});
try {
- await cdp.connect();
- assert.equal(cdp.connect_timing?.injector_source, "local_launch");
- assert.equal(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf");
- assert.match(cdp.ext_session_id ?? "", /^.+$/);
- const service_worker_url = await cdp.Mod.evaluate({
- expression: "chrome.runtime.getURL('modcdp/service_worker.js')",
- });
- assert.match(
- String(service_worker_url),
- /^chrome-extension:\/\/mdedooklbnfejodmnhmkdpkaedafkehf\/modcdp\/service_worker\.js$/,
- );
+ await assert.rejects(() => injector.prepare(), /escapes extension extraction directory/);
+ assert.equal(existsSync(path.join(temp_dir, "evil.txt")), false);
} finally {
- await cdp.close();
+ await injector.close();
+ rmSync(temp_dir, { recursive: true, force: true });
}
-}, 60_000);
+});
-test("LocalBrowserLaunchExtensionInjector prepares launcher config", async () => {
- const injector = new LocalBrowserLaunchExtensionInjector({ injector_extension_path: EXTENSION_PATH });
+test("LocalBrowserLaunchExtensionInjector prepares an unpacked extension directory for --load-extension", async () => {
+ const injector = new LocalBrowserLaunchExtensionInjector({
+ injector_extension_path: EXTENSION_PATH,
+ });
try {
await injector.prepare();
- const extra_args = injector.getLauncherConfig().extra_args ?? [];
- assert.equal(extra_args.length, 1);
- assert.match(extra_args[0], /^--load-extension=/);
+ const unpacked_extension_path = (injector as unknown as { unpacked_extension_path?: string | null })
+ .unpacked_extension_path;
+ assert.equal(typeof unpacked_extension_path, "string");
+ assert.notEqual(unpacked_extension_path, EXTENSION_PATH);
+ assert.equal(existsSync(path.join(unpacked_extension_path!, "manifest.json")), true);
+ assert.deepEqual(injector.getLauncherConfig(), {
+ extra_args: [`--load-extension=${unpacked_extension_path}`],
+ });
assert.equal(injector.options.injector_extension_id, DEFAULT_MODCDP_EXTENSION_ID);
} finally {
await injector.close();
}
});
-test("LocalBrowserLaunchExtensionInjector falls back to the default extension zip", async () => {
+test("LocalBrowserLaunchExtensionInjector prepares the default extension zip for --load-extension", async () => {
const injector = new LocalBrowserLaunchExtensionInjector();
try {
await injector.prepare();
- const extra_args = injector.getLauncherConfig().extra_args ?? [];
- assert.equal(extra_args.length, 1);
- assert.match(extra_args[0], /^--load-extension=.*modcdp-extension-/);
+ const unpacked_extension_path = (injector as unknown as { unpacked_extension_path?: string | null })
+ .unpacked_extension_path;
+ assert.equal(typeof unpacked_extension_path, "string");
+ assert.match(unpacked_extension_path!, /modcdp-extension-/);
+ assert.equal(existsSync(path.join(unpacked_extension_path!, "manifest.json")), true);
+ assert.deepEqual(injector.getLauncherConfig(), {
+ extra_args: [`--load-extension=${unpacked_extension_path}`],
+ });
assert.equal(injector.options.injector_extension_id, DEFAULT_MODCDP_EXTENSION_ID);
} finally {
await injector.close();
}
});
+
+test("LocalBrowserLaunchExtensionInjector returns immediately when the launched extension target is absent", async () => {
+ const methods: string[] = [];
+ const injector = new LocalBrowserLaunchExtensionInjector({
+ injector_extension_path: EXTENSION_PATH,
+ injector_trust_service_worker_target: true,
+ send: async (method) => {
+ methods.push(method);
+ if (method === "Target.getTargets") return { targetInfos: [] };
+ throw new Error(`unexpected ${method}`);
+ },
+ });
+
+ try {
+ await injector.prepare();
+ const started_at = performance.now();
+ const result = await injector.inject();
+ const elapsed_ms = performance.now() - started_at;
+ assert.equal(result, null);
+ assert.deepEqual(methods, ["Target.getTargets"]);
+ assert.equal(elapsed_ms < 200, true, `inject() took ${elapsed_ms}ms`);
+ } finally {
+ await injector.close();
+ }
+});
diff --git a/js/test/test.LocalBrowserLauncher.ts b/js/test/test.LocalBrowserLauncher.ts
index a644925..2924ba7 100644
--- a/js/test/test.LocalBrowserLauncher.ts
+++ b/js/test/test.LocalBrowserLauncher.ts
@@ -27,7 +27,6 @@ describe("LocalBrowserLauncher", () => {
const port = await LocalBrowserLauncher.freePort();
const chrome = await new LocalBrowserLauncher({
headless: true,
- sandbox: process.platform !== "linux",
chrome_ready_timeout_ms: 45_000,
chrome_ready_poll_interval_ms: 50,
}).launch({
@@ -61,9 +60,20 @@ describe("LocalBrowserLauncher", () => {
);
if (process.platform === "linux") {
expect((chrome.proc as { spawnargs?: string[] }).spawnargs ?? []).toContain("--no-sandbox");
+ } else {
+ expect((chrome.proc as { spawnargs?: string[] }).spawnargs ?? []).not.toContain("--no-sandbox");
}
await expect(stat(userDataDir)).resolves.toBeTruthy();
cdp = await CdpSocket.connect(chrome.cdp_url!);
+ const systemInfo = await cdp.send("SystemInfo.getInfo");
+ const commandLine = systemInfo.commandLine;
+ expect(commandLine).toEqual(expect.any(String));
+ expect(commandLine).toContain("--window-size=900,700");
+ if (process.platform === "linux") {
+ expect(commandLine).toContain("--no-sandbox");
+ } else {
+ expect(commandLine).not.toContain("--no-sandbox");
+ }
await expectCdpBrowserSurface(cdp);
} finally {
await cdp?.close();
@@ -82,7 +92,6 @@ describe("LocalBrowserLauncher", () => {
async () => {
const chrome = await new LocalBrowserLauncher().launch({
headless: true,
- sandbox: process.platform !== "linux",
remote_debugging: "pipe",
chrome_ready_timeout_ms: 45_000,
});
@@ -115,7 +124,6 @@ describe("LocalBrowserLauncher", () => {
async () => {
const chrome = await new LocalBrowserLauncher().launch({
headless: true,
- sandbox: process.platform !== "linux",
remote_debugging: "pipe",
loopback_cdp: true,
chrome_ready_timeout_ms: 45_000,
@@ -142,7 +150,6 @@ describe("LocalBrowserLauncher", () => {
const userDataDir = await mkdtemp(path.join(tmpdir(), "modcdp-local-profile-"));
const chrome = await new LocalBrowserLauncher({
headless: true,
- sandbox: process.platform !== "linux",
chrome_ready_timeout_ms: 45_000,
}).launch({
user_data_dir: userDataDir,
diff --git a/js/test/test.ModCDPClient.ts b/js/test/test.ModCDPClient.ts
index 69f3752..71de444 100644
--- a/js/test/test.ModCDPClient.ts
+++ b/js/test/test.ModCDPClient.ts
@@ -1,8 +1,10 @@
import assert from "node:assert/strict";
-import { existsSync } from "node:fs";
+import { existsSync, readdirSync, statSync } from "node:fs";
+import { homedir, platform } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "vitest";
+import { z } from "zod";
import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js";
import { ModCDPClient } from "../src/client/ModCDPClient.js";
@@ -10,6 +12,7 @@ import { CdpSocket } from "./helpers.BrowserLauncher.js";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
+const REVERSEWS_TEST_BROWSER_PATH = reversewsTestBrowserPath();
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -106,7 +109,14 @@ test("ModCDPClient dispatches root events before extension session is attached",
cdp._onRecv({
method: "Target.targetCreated",
params: {
- targetInfo: { targetId: "target-1", type: "page", url: "about:blank" },
+ targetInfo: {
+ targetId: "target-1",
+ type: "page",
+ title: "about:blank",
+ url: "about:blank",
+ attached: false,
+ canAccessOpener: false,
+ },
},
});
@@ -126,7 +136,14 @@ test("ModCDPClient event dispatch snapshots handlers when once removes itself",
cdp._onRecv({
method: "Target.targetCreated",
params: {
- targetInfo: { targetId: "target-1", type: "page", url: "about:blank" },
+ targetInfo: {
+ targetId: "target-1",
+ type: "page",
+ title: "about:blank",
+ url: "about:blank",
+ attached: false,
+ canAccessOpener: false,
+ },
},
});
assert.deepEqual(seen, ["once", "persistent"]);
@@ -135,17 +152,47 @@ test("ModCDPClient event dispatch snapshots handlers when once removes itself",
cdp._onRecv({
method: "Target.targetCreated",
params: {
- targetInfo: { targetId: "target-2", type: "page", url: "about:blank" },
+ targetInfo: {
+ targetId: "target-2",
+ type: "page",
+ title: "about:blank",
+ url: "about:blank",
+ attached: false,
+ canAccessOpener: false,
+ },
},
});
assert.deepEqual(seen, ["persistent"]);
});
+test("ModCDPClient validates native command params before sending", async () => {
+ const cdp = new ModCDPClient();
+
+ await assert.rejects(() => cdp.send("Runtime.evaluate", {}), /expression/);
+});
+
+test("ModCDPClient validates native and registered custom events before dispatch", async () => {
+ const cdp = new ModCDPClient();
+
+ assert.throws(() => {
+ cdp._onRecv({ method: "Target.targetCreated", params: {} });
+ }, /targetInfo/);
+
+ await cdp.Mod.addCustomEvent("Custom.ready", {
+ event_schema: { ok: z.boolean() },
+ });
+ assert.throws(() => {
+ cdp._onRecv({ method: "Custom.ready", params: { ok: "yes" } });
+ }, /boolean/);
+});
+
test("ModCDPClient connects with nested launch/upstream/extension/client/server config", async () => {
const cdp = new ModCDPClient({
launcher: {
launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
+ launcher_options: {
+ headless: true,
+ },
},
upstream: { upstream_mode: "ws" },
injector: {
@@ -180,26 +227,28 @@ test("ModCDPClient connects with nested launch/upstream/extension/client/server
assert.equal(cdp.upstream.upstream_mode, "ws");
assert.equal(cdp.upstream.upstream_reversews_wait_timeout_ms, 10_000);
assert.equal(cdp.injector.injector_mode, "auto");
- assert.equal(cdp.client.client_routes["*.*"], "direct_cdp");
- assert.equal(cdp.upstream_endpoint_kind, "raw_cdp");
- assert.match(cdp.cdp_url ?? "", /^ws:\/\//);
- await delay(2_000);
- const targets = (await cdp.sendRaw("Target.getTargets")) as {
- targetInfos: { type?: string; url?: string }[];
- };
assert.equal(
- targets.targetInfos.some(
- (target) =>
- target.type === "service_worker" &&
- target.url === `chrome-extension://${cdp.extension_id}/modcdp/service_worker.js`,
+ ["discovered", "local_launch", "extensions_load_unpacked", "borrowed"].includes(
+ String(cdp.connect_timing?.injector_source),
),
true,
);
+ assert.equal(cdp.client.client_routes["*.*"], "direct_cdp");
+ assert.equal(cdp.upstream_endpoint_kind, "raw_cdp");
+ assert.match(cdp.cdp_url ?? "", /^ws:\/\//);
+ const service_worker_url = await cdp.Mod.evaluate({
+ expression: "chrome.runtime.getURL('modcdp/service_worker.js')",
+ });
+ assert.equal(service_worker_url, `chrome-extension://${cdp.extension_id}/modcdp/service_worker.js`);
+ const contexts = (await cdp.Mod.evaluate({
+ expression:
+ "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => ({ type: context.contextType, url: context.documentUrl || context.origin || '' })))",
+ })) as { type?: string; url?: string }[];
assert.equal(
- targets.targetInfos.some(
- (target) =>
- target.type === "background_page" &&
- target.url === `chrome-extension://${cdp.extension_id}/offscreen/keepalive.html`,
+ contexts.some(
+ (context) =>
+ context.type === "OFFSCREEN_DOCUMENT" &&
+ context.url === `chrome-extension://${cdp.extension_id}/offscreen/keepalive.html`,
),
true,
);
@@ -251,7 +300,93 @@ test("ModCDPClient preserves explicit empty service worker suffix config", async
assert.deepEqual(cdp.injector.injector_service_worker_url_suffixes, []);
assert.deepEqual((await cdp._baseInjectorConfig()).injector_service_worker_url_suffixes, []);
-});
+}, 60_000);
+
+function reversewsTestBrowserPath() {
+ const explicit_candidates = [process.env.CHROME_PATH, platform() === "linux" ? "/usr/bin/chromium" : null].filter(
+ (candidate): candidate is string => Boolean(candidate),
+ );
+ for (const candidate of explicit_candidates) {
+ if (existsSync(candidate)) return candidate;
+ }
+ const home = homedir();
+ const patterns =
+ platform() === "darwin"
+ ? [
+ path.join(
+ home,
+ "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ ),
+ path.join(home, "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"),
+ path.join(
+ home,
+ "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ ),
+ ]
+ : platform() === "win32"
+ ? [
+ path.join(
+ process.env.LOCALAPPDATA || path.join(home, "AppData/Local"),
+ "ms-playwright/chromium-*/chrome-win*/chrome.exe",
+ ),
+ path.join(home, ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"),
+ ]
+ : [
+ path.join(home, ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"),
+ "/opt/pw-browsers/chromium-*/chrome-linux*/chrome",
+ path.join(home, ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"),
+ ];
+ const candidates = newestFirst(patterns.flatMap(expandGlob));
+ if (candidates[0]) return candidates[0];
+ throw new Error("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.");
+}
+
+function expandGlob(pattern: string) {
+ const normalized = path.normalize(pattern);
+ const { root } = path.parse(normalized);
+ const parts = normalized.slice(root.length).split(path.sep).filter(Boolean);
+ let candidates = [root || "."];
+ for (const part of parts) {
+ const has_wildcard = part.includes("*");
+ const matcher = has_wildcard ? wildcardToRegExp(part) : null;
+ const next: string[] = [];
+ for (const base of candidates) {
+ if (!existsSync(base)) continue;
+ if (!has_wildcard) {
+ const candidate = path.join(base, part);
+ if (existsSync(candidate)) next.push(candidate);
+ continue;
+ }
+ for (const child of readdirSync(base)) {
+ if (matcher!.test(child)) next.push(path.join(base, child));
+ }
+ }
+ candidates = next;
+ }
+ return candidates.filter((candidate) => existsSync(candidate));
+}
+
+function wildcardToRegExp(value: string) {
+ return new RegExp(`^${value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
+}
+
+function newestFirst(candidates: string[]) {
+ return [...new Set(candidates)].sort((a, b) => {
+ const left = scorePath(a);
+ const right = scorePath(b);
+ return right.version - left.version || right.mtime - left.mtime || a.localeCompare(b);
+ });
+}
+
+function scorePath(candidate: string) {
+ const numbers = candidate.match(/\d+/g)?.map(Number) ?? [];
+ const version = numbers.length > 0 ? Math.max(...numbers) : 0;
+ let mtime = 0;
+ try {
+ mtime = statSync(candidate).mtimeMs;
+ } catch {}
+ return { version, mtime };
+}
test("ModCDPClient defaults service worker suffix config to the ModCDP worker", async () => {
const cdp = new ModCDPClient();
@@ -281,7 +416,10 @@ test("ModCDPClient only exposes injector attach after CDP send is available", ()
test("ModCDPClient defaults launched ModCDP-server upstreams to extension auto", () => {
for (const mode of ["nativemessaging", "reversews", "nats"] as const) {
- const launched = new ModCDPClient({ launcher: { launcher_mode: "local" }, upstream: { upstream_mode: mode } });
+ const launched = new ModCDPClient({
+ launcher: { launcher_mode: "local" },
+ upstream: { upstream_mode: mode },
+ });
assert.equal(launched.launcher.launcher_mode, "local");
assert.equal(launched.upstream_endpoint_kind, "modcdp_server");
assert.equal(launched.injector.injector_mode, "auto");
@@ -293,6 +431,23 @@ test("ModCDPClient defaults launched ModCDP-server upstreams to extension auto",
}
});
+test("ModCDPClient orders local auto injection as launch flag then loadUnpacked fallback", async () => {
+ const cdp = new ModCDPClient({
+ launcher: { launcher_mode: "local" },
+ injector: { injector_mode: "auto" },
+ });
+
+ assert.deepEqual(
+ (await cdp._injectorsForConfig()).map((injector) => injector.constructor.name),
+ [
+ "LocalBrowserLaunchExtensionInjector",
+ "ExtensionsLoadUnpackedInjector",
+ "DiscoveredExtensionInjector",
+ "BorrowedExtensionInjector",
+ ],
+ );
+});
+
test("ModCDPClient rejects unknown component modes at their owning factory boundary", async () => {
await assert.rejects(
() =>
@@ -302,11 +457,17 @@ test("ModCDPClient rejects unknown component modes at their owning factory bound
/unknown upstream\.upstream_mode=bogus/,
);
await assert.rejects(
- () => new ModCDPClient({ launcher: { launcher_mode: "bogus" as any } })._browserLauncher(),
+ () =>
+ new ModCDPClient({
+ launcher: { launcher_mode: "bogus" as any },
+ })._browserLauncher(),
/unknown launcher\.launcher_mode=bogus/,
);
await assert.rejects(
- () => new ModCDPClient({ injector: { injector_mode: "bogus" as any } })._injectorsForConfig(),
+ () =>
+ new ModCDPClient({
+ injector: { injector_mode: "bogus" as any },
+ })._injectorsForConfig(),
/unknown injector\.injector_mode=bogus/,
);
});
@@ -314,7 +475,10 @@ test("ModCDPClient rejects unknown component modes at their owning factory bound
test("ModCDPClient.close does not close a remote browser it did not launch", async () => {
const chrome = await new LocalBrowserLauncher({
headless: true,
- sandbox: process.platform !== "linux",
+ chrome_ready_timeout_ms: 60_000,
+ // This test manually supplies --load-extension, so it intentionally uses
+ // the launch-flag browser path instead of relying on the client fallback.
+ executable_path: REVERSEWS_TEST_BROWSER_PATH,
extra_args: [`--load-extension=${EXTENSION_PATH}`],
}).launch();
const raw_cdp = await CdpSocket.connect(chrome.cdp_url!);
@@ -322,10 +486,14 @@ test("ModCDPClient.close does not close a remote browser it did not launch", asy
launcher: { launcher_mode: "remote" },
upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url },
injector: {
- injector_mode: "discover",
+ injector_mode: "auto",
+ injector_extension_path: EXTENSION_PATH,
injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
injector_trust_service_worker_target: true,
+ injector_service_worker_ready_timeout_ms: 30_000,
+ injector_service_worker_probe_timeout_ms: 30_000,
},
+ client: { client_routes: { "*.*": "direct_cdp" } },
});
try {
@@ -339,17 +507,22 @@ test("ModCDPClient.close does not close a remote browser it did not launch", asy
await cdp.close();
await chrome.close();
}
-}, 60_000);
+}, 180_000);
test("ModCDPClient.close keeps injector files until after launched browser shutdown", async () => {
const cdp = new ModCDPClient({
launcher: {
launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
+ launcher_options: {
+ headless: true,
+ // After explicit CHROME_PATH and CI /usr/bin/chromium, this test uses
+ // Chrome for Testing because Canary rejects --load-extension in this
+ // local launch injector path.
+ executable_path: REVERSEWS_TEST_BROWSER_PATH,
+ },
},
upstream: {
- upstream_mode: "reversews",
- upstream_reversews_wait_timeout_ms: 30_000,
+ upstream_mode: "ws",
},
injector: {
injector_mode: "auto",
@@ -396,7 +569,9 @@ test("ModCDPClient.close clears top-level connection state", async () => {
const cdp = new ModCDPClient({
launcher: {
launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
+ launcher_options: {
+ headless: true,
+ },
},
upstream: { upstream_mode: "ws" },
injector: {
@@ -413,4 +588,4 @@ test("ModCDPClient.close clears top-level connection state", async () => {
assert.equal(cdp.transport, null);
await assert.rejects(() => cdp.sendRaw("Browser.getVersion"), /ModCDP upstream is not connected/);
-});
+}, 60_000);
diff --git a/js/test/test.ModCDPClientCustomFlatNamespace.ts b/js/test/test.ModCDPClientCustomFlatNamespace.ts
index a391950..1f8f16f 100644
--- a/js/test/test.ModCDPClientCustomFlatNamespace.ts
+++ b/js/test/test.ModCDPClientCustomFlatNamespace.ts
@@ -5,6 +5,14 @@ import { test } from "vitest";
import { z } from "zod";
import { ModCDPClient, type ModCDPClientInstance } from "../src/client/ModCDPClient.js";
+import { installModCDPServer } from "../src/server/ModCDPServer.js";
+import type {
+ ModCDPCustomCommandRegistration,
+ ModCDPCustomEventRegistration,
+ ProtocolParams,
+ ProtocolPayload,
+ ProtocolResult,
+} from "../src/types/modcdp.js";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
@@ -16,7 +24,7 @@ test("custom commands install flat namespace methods through a real service work
const cdp = new ModCDPClient({
launcher: {
launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
+ launcher_options: { headless: true },
},
upstream: { upstream_mode: "ws" },
injector: {
@@ -56,7 +64,7 @@ test("custom events validate raw string handlers through a real service worker",
const cdp = new ModCDPClient({
launcher: {
launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
+ launcher_options: { headless: true },
},
upstream: { upstream_mode: "ws" },
injector: {
@@ -180,3 +188,32 @@ test("constructor custom command and event schemas validate nested payloads", ()
assert.deepEqual(event_schemas.get("Custom.count")?.parse({ value: 3 }), { value: 3 });
assert.throws(() => event_schemas.get("Custom.count")?.parse({ value: 0 }));
});
+
+test("service worker server validates registered custom command and event schemas", async () => {
+ const scope = {} as typeof globalThis;
+ const server = installModCDPServer(scope) as unknown as {
+ addCustomCommand(registration: ModCDPCustomCommandRegistration): ProtocolResult;
+ addCustomEvent(registration: ModCDPCustomEventRegistration): ProtocolResult;
+ handleCommand(method: string, params?: ProtocolParams, cdpSessionId?: string | null): Promise;
+ emit(eventName: string, payload?: ProtocolPayload, cdpSessionId?: string | null): Promise;
+ };
+
+ server.addCustomCommand({
+ name: "Custom.double",
+ params_schema: z.object({ value: z.number() }),
+ result_schema: z.object({ value: z.number() }),
+ handler: async (params: { value: number }) => ({ value: params.value * 2 }),
+ });
+ assert.deepEqual(await server.handleCommand("Custom.double", { value: 2 }), { value: 4 });
+ await assert.rejects(() => server.handleCommand("Custom.double", { value: "2" }));
+
+ server.addCustomCommand({
+ name: "Custom.badResult",
+ result_schema: z.object({ ok: z.boolean() }),
+ handler: async () => ({ ok: "yes" }),
+ });
+ await assert.rejects(() => server.handleCommand("Custom.badResult", {}));
+
+ server.addCustomEvent({ name: "Custom.ready", event_schema: z.object({ ok: z.boolean() }) });
+ await assert.rejects(() => server.emit("Custom.ready", { ok: "yes" }));
+});
diff --git a/js/test/test.ModCDPClientRoutedDefaultOverrides.ts b/js/test/test.ModCDPClientRoutedDefaultOverrides.ts
index e0c2ac5..24ba83a 100644
--- a/js/test/test.ModCDPClientRoutedDefaultOverrides.ts
+++ b/js/test/test.ModCDPClientRoutedDefaultOverrides.ts
@@ -3,9 +3,8 @@ import { test } from "vitest";
import path from "node:path";
import { fileURLToPath } from "node:url";
-import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js";
import { ModCDPClient } from "../src/client/ModCDPClient.js";
-import { commands, events } from "../src/types/generated/zod.js";
+import { events } from "../src/types/generated/zod.js";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
@@ -73,17 +72,27 @@ test(
"service-worker routed standard CDP commands and events can be transformed",
{ timeout: DEFAULT_ROUTED_OVERRIDES_TEST_TIMEOUT_MS },
async () => {
- const chrome = await new LocalBrowserLauncher().launch({
- headless: true,
- sandbox: process.platform !== "linux",
- extra_args: [`--load-extension=${EXTENSION_PATH}`],
+ const owner = new ModCDPClient({
+ launcher: {
+ launcher_mode: "local",
+ launcher_options: {
+ headless: true,
+ },
+ },
+ upstream: { upstream_mode: "ws" },
+ injector: {
+ injector_mode: "auto",
+ injector_extension_path: EXTENSION_PATH,
+ injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
+ injector_trust_service_worker_target: true,
+ },
});
+ await owner.connect();
const cdp = new ModCDPClient({
launcher: { launcher_mode: "remote" },
- upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url },
+ upstream: { upstream_mode: "ws", upstream_cdp_url: owner.cdp_url },
injector: {
- injector_mode: "auto",
- injector_extension_path: EXTENSION_PATH,
+ injector_mode: "discover",
injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
injector_trust_service_worker_target: true,
},
@@ -95,17 +104,17 @@ test(
},
},
server: {
- server_loopback_cdp_url: chrome.cdp_url,
+ server_loopback_cdp_url: owner.cdp_url,
server_routes: { "*.*": "loopback_cdp" },
},
});
try {
await cdp.connect();
- assert.equal(cdp.cdp_url, chrome.cdp_url);
- assert.equal(cdp.server.server_loopback_cdp_url, chrome.cdp_url);
+ assert.equal(cdp.cdp_url, owner.cdp_url);
+ assert.equal(cdp.server.server_loopback_cdp_url, owner.cdp_url);
- const rawTargets = commands["Target.getTargets"].result.parse(await cdp._sendMessage("Target.getTargets"));
+ const rawTargets = await cdp.send("Target.getTargets");
assert.ok(rawTargets.targetInfos?.length > 0, "expected raw Target.getTargets targetInfos");
assert.equal(
rawTargets.targetInfos.some((targetInfo) => Object.hasOwn(targetInfo, "tabId")),
@@ -187,7 +196,7 @@ test(
await cdp.Target.setDiscoverTargets({ discover: false });
} catch {}
await cdp.close();
- await chrome.close();
+ await owner.close();
}
},
);
diff --git a/js/test/test.NativeMessagingUpstreamTransport.ts b/js/test/test.NativeMessagingUpstreamTransport.ts
index d53e181..f51c600 100644
--- a/js/test/test.NativeMessagingUpstreamTransport.ts
+++ b/js/test/test.NativeMessagingUpstreamTransport.ts
@@ -1,7 +1,9 @@
import assert from "node:assert/strict";
import { once } from "node:events";
-import { existsSync } from "node:fs";
+import { existsSync, readdirSync, statSync } from "node:fs";
+import { mkdtemp, rm } from "node:fs/promises";
import net from "node:net";
+import { homedir, platform, tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "vitest";
@@ -11,6 +13,7 @@ import { ModCDPClient } from "../src/client/ModCDPClient.js";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
+const NATIVE_MESSAGING_TEST_BROWSER_PATH = extensionLaunchFlagTestBrowserPath();
const upstreamNativeMessagingHostName = (label: string) => `com.modcdp.test.${label}.${process.pid}`;
test("nativemessaging upstream config owns manifest, host, wait timeout, loopback, and injector config", async () => {
@@ -161,10 +164,19 @@ test("nativemessaging upstream accepts a replacement peer after disconnect", asy
test("nativemessaging upstream installs the launch-profile native host manifest and connects to a real extension", async () => {
const upstream_nativemessaging_host_name = "com.modcdp.bridge";
+ const profile_dir = await mkdtemp(path.join(tmpdir(), "modcdp.native."));
const native_client = new ModCDPClient({
launcher: {
launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
+ launcher_options: {
+ headless: true,
+ user_data_dir: profile_dir,
+ cleanup_user_data_dir: true,
+ // Native messaging is browser -> client only. After explicit CHROME_PATH
+ // and CI /usr/bin/chromium, this test uses Chrome for Testing because
+ // Canary rejects --load-extension in this local test path.
+ executable_path: NATIVE_MESSAGING_TEST_BROWSER_PATH,
+ },
},
upstream: {
upstream_mode: "nativemessaging",
@@ -204,6 +216,7 @@ test("nativemessaging upstream installs the launch-profile native host manifest
assert.equal(typeof second_version.product, "string");
} finally {
await native_client.close();
+ await rm(profile_dir, { recursive: true, force: true });
}
}, 90_000);
@@ -215,3 +228,89 @@ async function waitFor(predicate: () => boolean, timeout_ms = 2_000) {
}
throw new Error("Timed out waiting for condition");
}
+
+function extensionLaunchFlagTestBrowserPath() {
+ const explicit_candidates = [process.env.CHROME_PATH, platform() === "linux" ? "/usr/bin/chromium" : null].filter(
+ (candidate): candidate is string => Boolean(candidate),
+ );
+ for (const candidate of explicit_candidates) {
+ if (existsSync(candidate)) return candidate;
+ }
+ const home = homedir();
+ const patterns =
+ platform() === "darwin"
+ ? [
+ path.join(
+ home,
+ "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ ),
+ path.join(home, "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"),
+ path.join(
+ home,
+ "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ ),
+ ]
+ : platform() === "win32"
+ ? [
+ path.join(
+ process.env.LOCALAPPDATA || path.join(home, "AppData/Local"),
+ "ms-playwright/chromium-*/chrome-win*/chrome.exe",
+ ),
+ path.join(home, ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"),
+ ]
+ : [
+ path.join(home, ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"),
+ "/opt/pw-browsers/chromium-*/chrome-linux*/chrome",
+ path.join(home, ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"),
+ ];
+ const candidates = newestFirst(patterns.flatMap(expandGlob));
+ if (candidates[0]) return candidates[0];
+ throw new Error("Native messaging tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.");
+}
+
+function expandGlob(pattern: string) {
+ const normalized = path.normalize(pattern);
+ const { root } = path.parse(normalized);
+ const parts = normalized.slice(root.length).split(path.sep).filter(Boolean);
+ let candidates = [root || "."];
+ for (const part of parts) {
+ const has_wildcard = part.includes("*");
+ const matcher = has_wildcard ? wildcardToRegExp(part) : null;
+ const next: string[] = [];
+ for (const base of candidates) {
+ if (!existsSync(base)) continue;
+ if (!has_wildcard) {
+ const candidate = path.join(base, part);
+ if (existsSync(candidate)) next.push(candidate);
+ continue;
+ }
+ for (const child of readdirSync(base)) {
+ if (matcher!.test(child)) next.push(path.join(base, child));
+ }
+ }
+ candidates = next;
+ }
+ return candidates.filter((candidate) => existsSync(candidate));
+}
+
+function wildcardToRegExp(value: string) {
+ return new RegExp(`^${value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
+}
+
+function newestFirst(candidates: string[]) {
+ return [...new Set(candidates)].sort((a, b) => {
+ const left = scorePath(a);
+ const right = scorePath(b);
+ return right.version - left.version || right.mtime - left.mtime || a.localeCompare(b);
+ });
+}
+
+function scorePath(candidate: string) {
+ const numbers = candidate.match(/\d+/g)?.map(Number) ?? [];
+ const version = numbers.length > 0 ? Math.max(...numbers) : 0;
+ let mtime = 0;
+ try {
+ mtime = statSync(candidate).mtimeMs;
+ } catch {}
+ return { version, mtime };
+}
diff --git a/js/test/test.PipeUpstreamTransport.ts b/js/test/test.PipeUpstreamTransport.ts
index 7b31e7c..915e8a2 100644
--- a/js/test/test.PipeUpstreamTransport.ts
+++ b/js/test/test.PipeUpstreamTransport.ts
@@ -19,7 +19,7 @@ test("pipe upstream constructor, update, launcher config, and unconnected errors
assert.equal(transport.update({ cdp_url: "pipe://1234" }), transport);
assert.equal(transport.url, "pipe://1234");
await assert.rejects(() => transport.connect(), /upstream\.upstream_mode=pipe requires/);
- assert.throws(() => transport.send({ id: 1, method: "Browser.getVersion" }), /CDP pipe is not connected/);
+ assert.throws(() => transport.send({ id: 1, method: "Runtime.evaluate" }), /CDP pipe is not connected/);
});
test("pipe upstream resets connection state after pipe end and errors", async () => {
@@ -31,7 +31,7 @@ test("pipe upstream resets connection state after pipe end and errors", async ()
transport.onClose((error) => closed.push(error));
await transport.connect();
- transport.send({ id: 1, method: "Browser.getVersion", params: {} });
+ transport.send({ id: 1, method: "Runtime.evaluate", params: { expression: "1" } });
if (event_name === "end") pipe_read.emit("end");
else if (event_name === "read_error") pipe_read.emit("error", new Error("read failed"));
@@ -39,7 +39,7 @@ test("pipe upstream resets connection state after pipe end and errors", async ()
assert.equal(closed.length, 1);
assert.throws(
- () => transport.send({ id: 2, method: "Browser.getVersion", params: {} }),
+ () => transport.send({ id: 2, method: "Runtime.evaluate", params: { expression: "1" } }),
/CDP pipe is not connected/,
);
await transport.close();
@@ -50,15 +50,16 @@ test("pipe upstream launches a real browser and uses a pid-scoped pipe URL", asy
const cdp = new ModCDPClient({
launcher: {
launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
+ launcher_options: { headless: true },
},
upstream: { upstream_mode: "pipe" },
injector: {
- injector_mode: "auto",
+ injector_mode: "inject",
injector_extension_path: EXTENSION_PATH,
injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
injector_trust_service_worker_target: true,
},
+ server: { server_routes: { "*.*": "chrome_debugger" } },
});
try {
@@ -67,8 +68,12 @@ test("pipe upstream launches a real browser and uses a pid-scoped pipe URL", asy
assert.equal(cdp.upstream_endpoint_kind, "raw_cdp");
assert.match(cdp.cdp_url ?? "", /^pipe:\/\/\d+$/);
assert.equal(cdp.transport?.url, cdp.cdp_url);
- const version = (await cdp.sendRaw("Browser.getVersion")) as Record;
- assert.equal(typeof version.product, "string");
+ await cdp.Mod.addCustomCommand("Custom.runtimeReadyState", {
+ expression:
+ "async () => await cdp.send('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true })",
+ });
+ const runtime = (await cdp.send("Custom.runtimeReadyState")) as { result?: { value?: unknown } };
+ assert.equal(runtime.result?.value, "complete");
} finally {
await cdp.close();
}
diff --git a/js/test/test.RemoteBrowserLauncher.ts b/js/test/test.RemoteBrowserLauncher.ts
index d245df9..b9bcb64 100644
--- a/js/test/test.RemoteBrowserLauncher.ts
+++ b/js/test/test.RemoteBrowserLauncher.ts
@@ -20,7 +20,6 @@ describe("RemoteBrowserLauncher", () => {
const local = await new LocalBrowserLauncher().launch({
port: await LocalBrowserLauncher.freePort(),
headless: true,
- sandbox: process.platform !== "linux",
chrome_ready_timeout_ms: 45_000,
});
let cdp: CdpSocket | null = null;
@@ -61,12 +60,10 @@ describe("RemoteBrowserLauncher", () => {
const first = await new LocalBrowserLauncher().launch({
port: await LocalBrowserLauncher.freePort(),
headless: true,
- sandbox: process.platform !== "linux",
});
const second = await new LocalBrowserLauncher().launch({
port: await LocalBrowserLauncher.freePort(),
headless: true,
- sandbox: process.platform !== "linux",
});
try {
diff --git a/js/test/test.ReverseWebSocketUpstreamTransport.ts b/js/test/test.ReverseWebSocketUpstreamTransport.ts
index e3cdff5..3104255 100644
--- a/js/test/test.ReverseWebSocketUpstreamTransport.ts
+++ b/js/test/test.ReverseWebSocketUpstreamTransport.ts
@@ -1,7 +1,9 @@
import assert from "node:assert/strict";
import { once } from "node:events";
+import { existsSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
+import { homedir, platform } from "node:os";
import WebSocket from "ws";
import { test } from "vitest";
@@ -11,14 +13,15 @@ import { ModCDPClient } from "../src/client/ModCDPClient.js";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
+const REVERSEWS_TEST_BROWSER_PATH = reversewsTestBrowserPath();
-test("reversews upstream config owns bind updates, wait timeout, and injector config", async () => {
+test("reversews upstream config owns bind updates and wait timeout", async () => {
const transport = new ReverseWebSocketUpstreamTransport({
upstream_reversews_bind: "127.0.0.1:29292",
upstream_reversews_wait_timeout_ms: 10,
});
assert.equal(transport.url, "ws://127.0.0.1:29292");
- assert.deepEqual(transport.getInjectorConfig(), { upstream_reversews_url: "ws://127.0.0.1:29292" });
+ assert.deepEqual(transport.getInjectorConfig(), {});
assert.equal(
transport.update({
upstream_reversews_bind: "127.0.0.1:29293",
@@ -27,7 +30,7 @@ test("reversews upstream config owns bind updates, wait timeout, and injector co
transport,
);
assert.equal(transport.url, "ws://127.0.0.1:29293");
- assert.deepEqual(transport.getInjectorConfig(), { upstream_reversews_url: "ws://127.0.0.1:29293" });
+ assert.deepEqual(transport.getInjectorConfig(), {});
assert.throws(
() => transport.send({ id: 1, method: "Browser.getVersion" }),
/No reverse ModCDP extension peer is connected/,
@@ -129,48 +132,139 @@ test("reversews upstream accepts a replacement peer after disconnect", async ()
}
});
-test("reversews upstream accepts a real extension reverse connection and routes CDP through loopback", async () => {
- const reverse_bind = "127.0.0.1:29292";
- const reverse_url = `ws://${reverse_bind}`;
- const reverse = new ModCDPClient({
+test("reversews upstream accepts a real extension reverse connection and routes CDP through chrome debugger", async () => {
+ const cdp = new ModCDPClient({
launcher: {
launcher_mode: "local",
launcher_options: {
headless: process.platform === "linux" && !process.env.DISPLAY,
- sandbox: process.platform !== "linux",
+ // Reversews is browser -> client only. After explicit CHROME_PATH and
+ // CI /usr/bin/chromium, these tests use Chrome for Testing because
+ // Canary rejects --load-extension in this local test path.
+ executable_path: REVERSEWS_TEST_BROWSER_PATH,
},
},
- upstream: { upstream_mode: "reversews", upstream_reversews_bind: reverse_bind },
+ upstream: { upstream_mode: "reversews" },
injector: {
injector_mode: "auto",
injector_extension_path: EXTENSION_PATH,
injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
injector_trust_service_worker_target: true,
- },
- server: {
- server_routes: { "*.*": "loopback_cdp" },
+ injector_service_worker_probe_timeout_ms: 1_000,
},
});
try {
- await reverse.connect();
- assert.equal(reverse.transport?.mode, "reversews");
- assert.equal(reverse.upstream_endpoint_kind, "modcdp_server");
- assert.equal(reverse.transport?.url, reverse_url);
+ await cdp.connect();
+ assert.equal(cdp.transport?.mode, "reversews");
+ assert.equal(cdp.upstream_endpoint_kind, "modcdp_server");
+ assert.equal(cdp.transport?.url, "ws://127.0.0.1:29292");
assert.equal(
- (reverse.transport as ReverseWebSocketUpstreamTransport).peer_info?.extension_id,
+ (cdp.transport as ReverseWebSocketUpstreamTransport).peer_info?.extension_id,
"mdedooklbnfejodmnhmkdpkaedafkehf",
);
- const version = (await reverse.send("Browser.getVersion")) as Record;
- assert.equal(typeof version.product, "string");
+ const evaluated = (await cdp.send("Runtime.evaluate", {
+ expression: "location.href",
+ returnByValue: true,
+ })) as { result?: { value?: unknown } };
+ assert.equal(evaluated.result?.value, "about:blank");
await new Promise((resolve) => setTimeout(resolve, 1_500));
- const second_version = (await reverse.send("Browser.getVersion")) as Record;
- assert.equal(typeof second_version.product, "string");
+ const second_evaluated = (await cdp.send("Runtime.evaluate", {
+ expression: "document.readyState",
+ returnByValue: true,
+ })) as { result?: { value?: unknown } };
+ assert.equal(second_evaluated.result?.value, "complete");
} finally {
- await reverse.close();
+ await cdp.close();
}
}, 60_000);
+function reversewsTestBrowserPath() {
+ const explicit_candidates = [process.env.CHROME_PATH, platform() === "linux" ? "/usr/bin/chromium" : null].filter(
+ (candidate): candidate is string => Boolean(candidate),
+ );
+ for (const candidate of explicit_candidates) {
+ if (existsSync(candidate)) return candidate;
+ }
+ const home = homedir();
+ const patterns =
+ platform() === "darwin"
+ ? [
+ path.join(
+ home,
+ "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ ),
+ path.join(home, "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"),
+ path.join(
+ home,
+ "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ ),
+ ]
+ : platform() === "win32"
+ ? [
+ path.join(
+ process.env.LOCALAPPDATA || path.join(home, "AppData/Local"),
+ "ms-playwright/chromium-*/chrome-win*/chrome.exe",
+ ),
+ path.join(home, ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"),
+ ]
+ : [
+ path.join(home, ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"),
+ "/opt/pw-browsers/chromium-*/chrome-linux*/chrome",
+ path.join(home, ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"),
+ ];
+ const candidates = newestFirst(patterns.flatMap(expandGlob));
+ if (candidates[0]) return candidates[0];
+ throw new Error("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.");
+}
+
+function expandGlob(pattern: string) {
+ const normalized = path.normalize(pattern);
+ const { root } = path.parse(normalized);
+ const parts = normalized.slice(root.length).split(path.sep).filter(Boolean);
+ let candidates = [root || "."];
+ for (const part of parts) {
+ const has_wildcard = part.includes("*");
+ const matcher = has_wildcard ? wildcardToRegExp(part) : null;
+ const next: string[] = [];
+ for (const base of candidates) {
+ if (!existsSync(base)) continue;
+ if (!has_wildcard) {
+ const candidate = path.join(base, part);
+ if (existsSync(candidate)) next.push(candidate);
+ continue;
+ }
+ for (const child of readdirSync(base)) {
+ if (matcher!.test(child)) next.push(path.join(base, child));
+ }
+ }
+ candidates = next;
+ }
+ return candidates.filter((candidate) => existsSync(candidate));
+}
+
+function wildcardToRegExp(value: string) {
+ return new RegExp(`^${value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
+}
+
+function newestFirst(candidates: string[]) {
+ return [...new Set(candidates)].sort((a, b) => {
+ const left = scorePath(a);
+ const right = scorePath(b);
+ return right.version - left.version || right.mtime - left.mtime || a.localeCompare(b);
+ });
+}
+
+function scorePath(candidate: string) {
+ const numbers = candidate.match(/\d+/g)?.map(Number) ?? [];
+ const version = numbers.length > 0 ? Math.max(...numbers) : 0;
+ let mtime = 0;
+ try {
+ mtime = statSync(candidate).mtimeMs;
+ } catch {}
+ return { version, mtime };
+}
+
async function waitFor(predicate: () => boolean, timeout_ms = 2_000) {
const deadline = Date.now() + timeout_ms;
while (Date.now() < deadline) {
diff --git a/js/test/test.WebSocketUpstreamTransport.ts b/js/test/test.WebSocketUpstreamTransport.ts
index 01dc8b2..1f2ef98 100644
--- a/js/test/test.WebSocketUpstreamTransport.ts
+++ b/js/test/test.WebSocketUpstreamTransport.ts
@@ -26,7 +26,7 @@ test("ws upstream launches a real browser and speaks raw CDP", async () => {
const cdp = new ModCDPClient({
launcher: {
launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
+ launcher_options: { headless: true },
},
upstream: { upstream_mode: "ws" },
injector: {
@@ -43,9 +43,16 @@ test("ws upstream launches a real browser and speaks raw CDP", async () => {
assert.equal(cdp.upstream_endpoint_kind, "raw_cdp");
assert.equal(cdp.connect_timing?.upstream_mode, "ws");
assert.equal(cdp.connect_timing?.upstream_endpoint_kind, "raw_cdp");
+ const connect_timing = cdp.connect_timing as
+ | {
+ transport_connected_at: number;
+ transport_duration_ms: number;
+ transport_started_at: number;
+ }
+ | undefined;
assert.equal(
- cdp.connect_timing?.transport_duration_ms,
- (cdp.connect_timing?.transport_connected_at ?? 0) - (cdp.connect_timing?.transport_started_at ?? 0),
+ connect_timing?.transport_duration_ms,
+ (connect_timing?.transport_connected_at ?? 0) - (connect_timing?.transport_started_at ?? 0),
);
assert.match(cdp.cdp_url ?? "", /^ws:\/\//);
const version = (await cdp.sendRaw("Browser.getVersion")) as Record;
@@ -74,7 +81,6 @@ test("ws upstream launches a real browser and speaks raw CDP", async () => {
test("ws upstream resolves a bare host:port CDP endpoint to the browser websocket", async () => {
const chrome = await new LocalBrowserLauncher({
headless: true,
- sandbox: process.platform !== "linux",
}).launch();
const transport = new WebSocketUpstreamTransport({ cdp_url: `127.0.0.1:${chrome.port}` });
@@ -98,7 +104,6 @@ test("ws upstream resolves a bare host:port CDP endpoint to the browser websocke
test("ws upstream close clears connection state", async () => {
const chrome = await new LocalBrowserLauncher({
headless: true,
- sandbox: process.platform !== "linux",
}).launch();
const transport = new WebSocketUpstreamTransport({ cdp_url: chrome.cdp_url });
diff --git a/js/test/test.proxy.ts b/js/test/test.proxy.ts
index 9a02837..e0c9190 100644
--- a/js/test/test.proxy.ts
+++ b/js/test/test.proxy.ts
@@ -1,23 +1,24 @@
import assert from "node:assert/strict";
import { spawn, type ChildProcess } from "node:child_process";
import { once } from "node:events";
+import { existsSync, readdirSync, statSync } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
-import { tmpdir } from "node:os";
+import { homedir, platform, tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "vitest";
+import { ModCDPClient } from "../src/client/ModCDPClient.js";
import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js";
import { startProxy } from "../src/proxy/proxy.js";
-import { ModCDPClient } from "../src/client/ModCDPClient.js";
import { CdpSocket } from "./helpers.BrowserLauncher.js";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
const LOCAL_TEST_LAUNCH_OPTIONS = {
headless: true,
- sandbox: process.platform !== "linux",
};
+const REVERSEWS_TEST_BROWSER_PATH = reversewsTestBrowserPath();
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -64,14 +65,34 @@ async function expectProxyCdpWorks(proxy_url: string, transport: string) {
const cdp = await CdpSocket.connect(proxy_url);
let target_id: string | null = null;
try {
- const version = await cdp.send("Browser.getVersion");
- assert.equal(typeof version.product, "string");
-
const evaluated = await cdp.send("Mod.evaluate", {
expression: `({ ok: true, transport: ${JSON.stringify(transport)} })`,
});
assert.deepEqual(evaluated, { ok: true, transport });
+ if (transport.includes("reversews")) {
+ const runtime = await cdp.send("Runtime.evaluate", {
+ expression: "document.readyState",
+ returnByValue: true,
+ });
+ assert.equal((runtime.result as { value?: unknown } | undefined)?.value, "complete");
+ return;
+ }
+
+ if (transport.includes("pipe")) {
+ await cdp.send("Mod.addCustomCommand", {
+ name: "Custom.runtimeReadyState",
+ expression:
+ "async () => await cdp.send('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true })",
+ });
+ const runtime = await cdp.send("Custom.runtimeReadyState");
+ assert.equal((runtime.result as { value?: unknown } | undefined)?.value, "complete");
+ return;
+ }
+
+ const version = await cdp.send("Browser.getVersion");
+ assert.equal(typeof version.product, "string");
+
const created = await cdp.send("Target.createTarget", {
url: `about:blank#modcdp-proxy-${transport}`,
});
@@ -118,11 +139,11 @@ test("proxy upgrades a vanilla CDP websocket to ModCDP against a real browser ov
},
upstream: { upstream_mode: "pipe" },
injector: {
- injector_mode: "auto",
+ injector_mode: "inject",
injector_extension_path: EXTENSION_PATH,
},
server: {
- server_routes: { "*.*": "loopback_cdp" },
+ server_routes: { "*.*": "chrome_debugger" },
},
});
@@ -144,17 +165,17 @@ test("proxy CLI maps user-facing flags into a real pipe upstream browser session
String(proxy_port),
"--launcher-mode=local",
"--launcher-options",
- JSON.stringify({ headless: true, sandbox: process.platform !== "linux" }),
+ JSON.stringify({ headless: true }),
"--upstream-mode=pipe",
- "--injector-mode=auto",
+ "--injector-mode=inject",
+ "--injector-extension-path",
+ EXTENSION_PATH,
"--injector-service-worker-url-suffixes",
JSON.stringify(["/modcdp/service_worker.js"]),
"--injector-trust-service-worker-target",
"true",
- "--injector-service-worker-probe-timeout-ms",
- "30000",
"--server-routes",
- JSON.stringify({ "*.*": "loopback_cdp" }),
+ JSON.stringify({ "*.*": "chrome_debugger" }),
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
@@ -184,7 +205,7 @@ test("proxy CLI maps local ws launch without requiring upstream ws url", async (
"--launcher-user-data-dir",
user_data_dir,
"--launcher-options",
- JSON.stringify({ headless: true, sandbox: process.platform !== "linux" }),
+ JSON.stringify({ headless: true }),
"--upstream-mode=ws",
"--injector-mode=auto",
"--injector-extension-path",
@@ -213,11 +234,20 @@ test("proxy CLI maps local ws launch without requiring upstream ws url", async (
}, 60_000);
test("proxy CLI maps ws upstream URL and route shorthands into an existing real browser", async () => {
- const chrome = await new LocalBrowserLauncher({
- headless: true,
- sandbox: process.platform !== "linux",
- extra_args: [`--load-extension=${EXTENSION_PATH}`],
- }).launch();
+ const owner = new ModCDPClient({
+ launcher: {
+ launcher_mode: "local",
+ launcher_options: LOCAL_TEST_LAUNCH_OPTIONS,
+ },
+ upstream: { upstream_mode: "ws" },
+ injector: {
+ injector_mode: "auto",
+ injector_extension_path: EXTENSION_PATH,
+ injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
+ injector_trust_service_worker_target: true,
+ },
+ });
+ await owner.connect();
const proxy_port = await LocalBrowserLauncher.freePort();
const proxy_script = path.resolve(HERE, "..", "..", "dist", "js", "src", "proxy", "proxy.js");
const proc = spawn(
@@ -229,7 +259,7 @@ test("proxy CLI maps ws upstream URL and route shorthands into an existing real
"--launcher-mode=remote",
"--upstream-mode=ws",
"--upstream-cdp-url",
- chrome.cdp_url!,
+ owner.cdp_url!,
"--injector-mode=discover",
"--client-routes",
JSON.stringify({
@@ -248,11 +278,11 @@ test("proxy CLI maps ws upstream URL and route shorthands into an existing real
await expectProxyCdpWorks(`ws://127.0.0.1:${proxy_port}/devtools/browser/proxy`, "cli-ws");
} finally {
await closeProcess(proc);
- await chrome.close();
+ await owner.close();
}
}, 60_000);
-test("proxy CLI maps user-facing flags into a real reversews local launch", async () => {
+test("proxy CLI maps user-facing flags into a real reversews browser session", async () => {
const proxy_port = await LocalBrowserLauncher.freePort();
const proxy_script = path.resolve(HERE, "..", "..", "dist", "js", "src", "proxy", "proxy.js");
const proc = spawn(
@@ -263,15 +293,25 @@ test("proxy CLI maps user-facing flags into a real reversews local launch", asyn
String(proxy_port),
"--launcher-mode=local",
"--launcher-options",
- JSON.stringify({ headless: true, sandbox: process.platform !== "linux" }),
+ JSON.stringify({
+ ...LOCAL_TEST_LAUNCH_OPTIONS,
+ // Reversews is browser -> client only. After explicit CHROME_PATH and
+ // CI /usr/bin/chromium, these tests use Chrome for Testing because
+ // Canary rejects --load-extension in this local test path.
+ executable_path: REVERSEWS_TEST_BROWSER_PATH,
+ }),
"--upstream-mode=reversews",
"--upstream-reversews-wait-timeout-ms",
"10000",
"--injector-mode=auto",
"--injector-extension-path",
EXTENSION_PATH,
- "--server-routes",
- JSON.stringify({ "*.*": "loopback_cdp" }),
+ "--injector-service-worker-url-suffixes",
+ JSON.stringify(["/modcdp/service_worker.js"]),
+ "--injector-trust-service-worker-target",
+ "true",
+ "--injector-service-worker-probe-timeout-ms",
+ "1000",
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
@@ -286,105 +326,117 @@ test("proxy CLI maps user-facing flags into a real reversews local launch", asyn
test("proxy upgrades a vanilla CDP websocket to ModCDP against a real browser over reversews upstream", async () => {
const proxy_port = await LocalBrowserLauncher.freePort();
- const reverse_port = await LocalBrowserLauncher.freePort();
- const reverse_bind = `127.0.0.1:${reverse_port}`;
- const reverse_url = `ws://${reverse_bind}`;
const proxy = await startProxy({
port: proxy_port,
- upstream: { upstream_mode: "reversews", upstream_reversews_bind: reverse_bind },
- server: {
- server_routes: { "*.*": "loopback_cdp" },
- },
- });
- const bootstrap = new ModCDPClient({
launcher: {
launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
+ launcher_options: {
+ ...LOCAL_TEST_LAUNCH_OPTIONS,
+ // Reversews is browser -> client only. After explicit CHROME_PATH and
+ // CI /usr/bin/chromium, these tests use Chrome for Testing because
+ // Canary rejects --load-extension in this local test path.
+ executable_path: REVERSEWS_TEST_BROWSER_PATH,
+ },
},
- upstream: { upstream_mode: "ws" },
+ upstream: { upstream_mode: "reversews", upstream_reversews_wait_timeout_ms: 10_000 },
injector: {
injector_mode: "auto",
injector_extension_path: EXTENSION_PATH,
injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"],
injector_trust_service_worker_target: true,
- },
- server: {
- server_routes: { "*.*": "loopback_cdp" },
+ injector_service_worker_probe_timeout_ms: 1_000,
},
});
try {
- await bootstrap.connect();
- await bootstrap.send("Mod.evaluate", {
- params: { reverse_url },
- expression: "async ({ reverse_url }) => ModCDP.startReverseBridge(reverse_url)",
- });
await expectProxyCdpWorks(proxy.url, "reversews");
} finally {
- await bootstrap.close();
await proxy.close();
}
}, 90_000);
-test("proxy reversews local launch auto-injects the extension through the real client path", async () => {
- const proxy_port = await LocalBrowserLauncher.freePort();
- const proxy = await startProxy({
- port: proxy_port,
- launcher: {
- launcher_mode: "local",
- launcher_options: { headless: true, sandbox: process.platform !== "linux" },
- },
- upstream: {
- upstream_mode: "reversews",
- upstream_reversews_wait_timeout_ms: 10_000,
- },
- injector: {
- injector_mode: "auto",
- injector_extension_path: EXTENSION_PATH,
- },
- server: {
- server_routes: { "*.*": "loopback_cdp" },
- },
- });
+function reversewsTestBrowserPath() {
+ const explicit_candidates = [process.env.CHROME_PATH, platform() === "linux" ? "/usr/bin/chromium" : null].filter(
+ (candidate): candidate is string => Boolean(candidate),
+ );
+ for (const candidate of explicit_candidates) {
+ if (existsSync(candidate)) return candidate;
+ }
+ const home = homedir();
+ const patterns =
+ platform() === "darwin"
+ ? [
+ path.join(
+ home,
+ "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ ),
+ path.join(home, "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"),
+ path.join(
+ home,
+ "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
+ ),
+ ]
+ : platform() === "win32"
+ ? [
+ path.join(
+ process.env.LOCALAPPDATA || path.join(home, "AppData/Local"),
+ "ms-playwright/chromium-*/chrome-win*/chrome.exe",
+ ),
+ path.join(home, ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"),
+ ]
+ : [
+ path.join(home, ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"),
+ "/opt/pw-browsers/chromium-*/chrome-linux*/chrome",
+ path.join(home, ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"),
+ ];
+ const candidates = newestFirst(patterns.flatMap(expandGlob));
+ if (candidates[0]) return candidates[0];
+ throw new Error("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.");
+}
- try {
- await expectProxyCdpWorks(proxy.url, "reversews-local-launch");
- } finally {
- await proxy.close();
+function expandGlob(pattern: string) {
+ const normalized = path.normalize(pattern);
+ const { root } = path.parse(normalized);
+ const parts = normalized.slice(root.length).split(path.sep).filter(Boolean);
+ let candidates = [root || "."];
+ for (const part of parts) {
+ const has_wildcard = part.includes("*");
+ const matcher = has_wildcard ? wildcardToRegExp(part) : null;
+ const next: string[] = [];
+ for (const base of candidates) {
+ if (!existsSync(base)) continue;
+ if (!has_wildcard) {
+ const candidate = path.join(base, part);
+ if (existsSync(candidate)) next.push(candidate);
+ continue;
+ }
+ for (const child of readdirSync(base)) {
+ if (matcher!.test(child)) next.push(path.join(base, child));
+ }
+ }
+ candidates = next;
}
-}, 90_000);
+ return candidates.filter((candidate) => existsSync(candidate));
+}
-test("proxy passes custom extension discovery config through to ModCDPClient", async () => {
- const proxy_port = await LocalBrowserLauncher.freePort();
- const reverse_port = await LocalBrowserLauncher.freePort();
- await assert.rejects(
- () =>
- startProxy({
- port: proxy_port,
- launcher: {
- launcher_mode: "local",
- launcher_options: {
- headless: true,
- sandbox: process.platform !== "linux",
- extra_args: [`--load-extension=${EXTENSION_PATH}`],
- },
- },
- upstream: {
- upstream_mode: "reversews",
- upstream_reversews_bind: `127.0.0.1:${reverse_port}`,
- upstream_reversews_wait_timeout_ms: 1_000,
- },
- injector: {
- injector_mode: "discover",
- injector_extension_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- injector_require_service_worker_target: true,
- injector_service_worker_probe_timeout_ms: 200,
- injector_service_worker_ready_timeout_ms: 200,
- },
- server: {
- server_routes: { "*.*": "loopback_cdp" },
- },
- }),
- /Timed out waiting 1000ms for reverse ModCDP extension connection/,
- );
-}, 60_000);
+function wildcardToRegExp(value: string) {
+ return new RegExp(`^${value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
+}
+
+function newestFirst(candidates: string[]) {
+ return [...new Set(candidates)].sort((a, b) => {
+ const left = scorePath(a);
+ const right = scorePath(b);
+ return right.version - left.version || right.mtime - left.mtime || a.localeCompare(b);
+ });
+}
+
+function scorePath(candidate: string) {
+ const numbers = candidate.match(/\d+/g)?.map(Number) ?? [];
+ const version = numbers.length > 0 ? Math.max(...numbers) : 0;
+ let mtime = 0;
+ try {
+ mtime = statSync(candidate).mtimeMs;
+ } catch {}
+ return { version, mtime };
+}
diff --git a/js/test/test.translate.ts b/js/test/test.translate.ts
index fa122de..df9981f 100644
--- a/js/test/test.translate.ts
+++ b/js/test/test.translate.ts
@@ -25,12 +25,30 @@ test("translate routes, wraps, and unwraps ModCDP protocol messages deterministi
);
assert.equal(wrapped.target, "service_worker");
assert.equal(wrapped.steps[0]?.method, "Runtime.callFunctionOn");
- assert.match(String(wrapped.steps[0]?.params.functionDeclaration), /attachToSession\("session-1"\)/);
+ const wrapped_step_params = wrapped.steps[0]?.params as { functionDeclaration?: unknown } | undefined;
+ assert.match(String(wrapped_step_params?.functionDeclaration), /attachToSession\("session-1"\)/);
assert.equal(wrapped.steps[0]?.unwrap, "runtime");
const configured = wrapCommandIfNeeded("Mod.configure", { server: { server_routes: { "*.*": "loopback_cdp" } } });
assert.equal(configured.steps[0]?.unwrap, "runtime_json");
+ const custom = wrapCommandIfNeeded(
+ "Custom.echo",
+ { secret: "x".repeat(100), nested: { ok: true } },
+ { cdpSessionId: "session-1" },
+ );
+ const custom_step_params = custom.steps[0]?.params as
+ | { arguments?: Array<{ value?: unknown }>; functionDeclaration?: unknown }
+ | undefined;
+ assert.match(String(custom_step_params?.functionDeclaration), /JSON\.parse\(paramsJson\)/);
+ assert.doesNotMatch(String(custom_step_params?.functionDeclaration), /xxxxxxxxxx/);
+ assert.equal(custom_step_params?.arguments?.[0]?.value, "Custom.echo");
+ assert.deepEqual(JSON.parse(String(custom_step_params?.arguments?.[1]?.value)), {
+ secret: "x".repeat(100),
+ nested: { ok: true },
+ });
+ assert.equal(custom_step_params?.arguments?.[2]?.value, "session-1");
+
assert.deepEqual(unwrapResponseIfNeeded({ result: { type: "object", value: { ok: true } } }, "runtime"), {
ok: true,
});
diff --git a/package.json b/package.json
index e706aa5..a9aa013 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "modcdp",
- "version": "0.0.13",
+ "version": "0.0.19",
"repository": {
"type": "git",
"url": "git+https://github.com/pirate/modcdp.git"
@@ -8,6 +8,8 @@
"files": [
"js/scripts/**/*.mjs",
"dist/**/*",
+ "dist/extension/**/*",
+ "dist/js/**/*",
"dist/extension.zip",
"extension/src/**/*.ts",
"extension/src/pages/**/*",
@@ -132,7 +134,7 @@
"clean": "rm -rf dist",
"build": "pnpm run build:package && pnpm run build:extension-assets && pnpm run build:python-readme",
"build:clean": "pnpm run clean && pnpm run build",
- "build:package": "tsc -p tsconfig.json && rm -rf dist/js/test",
+ "build:package": "tsc -p tsconfig.json && rm -rf dist/js/test && rm -f dist/.gitignore",
"build:extension-assets": "node js/scripts/build-extension-assets.mjs",
"build:python-readme": "mkdir -p python && if [ README.md -ef python/README.md ]; then :; else cp README.md python/README.md; fi",
"typecheck": "tsc -p tsconfig.json --noEmit",
@@ -159,6 +161,7 @@
"@types/chrome": "^0.1.40",
"@types/node": "^25.6.0",
"@types/ws": "^8.18.1",
+ "esbuild": "^0.28.0",
"oxfmt": "^0.47.0",
"oxlint": "^1.62.0",
"playwright": "^1.59.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 159662b..58943a0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -27,6 +27,9 @@ importers:
'@types/ws':
specifier: ^8.18.1
version: 8.18.1
+ esbuild:
+ specifier: ^0.28.0
+ version: 0.28.0
oxfmt:
specifier: ^0.47.0
version: 0.47.0
@@ -44,7 +47,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^4.1.5
- version: 4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0))
+ version: 4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0))
optionalDependencies:
ws:
specifier: ^8.18.0
@@ -101,6 +104,162 @@ packages:
resolution: {integrity: sha512-23pSHE9+ZEPa5wXGeAcd8tA0Dn8Ba+pVsnjKVk4VcnJ9s1soEDt7197R5p87hPpw4X52uoBzFFlC7HGIPhFYFA==}
engines: {bun: '>=1', deno: '>=2', node: '>=22'}
+ '@esbuild/aix-ppc64@0.28.0':
+ resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.28.0':
+ resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.28.0':
+ resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.28.0':
+ resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.28.0':
+ resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.28.0':
+ resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.28.0':
+ resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.28.0':
+ resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.28.0':
+ resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.28.0':
+ resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.28.0':
+ resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.28.0':
+ resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.28.0':
+ resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.28.0':
+ resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.28.0':
+ resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.28.0':
+ resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.28.0':
+ resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.28.0':
+ resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.28.0':
+ resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.28.0':
+ resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.28.0':
+ resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.28.0':
+ resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.28.0':
+ resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.28.0':
+ resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.28.0':
+ resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.28.0':
+ resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
'@j178/prek-darwin-arm64@0.3.10':
resolution: {integrity: sha512-7KHluUGx5lAEtRKnleVSmZGsc9JNeVMRLbSngwU25e4XiJNdczVPrD5EcNwjngh4qoAm5xs05LE/waH7Kcowrg==}
engines: {node: '>=18'}
@@ -772,6 +931,11 @@ packages:
es-module-lexer@2.1.0:
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+ esbuild@0.28.0:
+ resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -1308,6 +1472,84 @@ snapshots:
'@eplightning/nats-server-win32-arm64': 2.12.5
'@eplightning/nats-server-win32-x64': 2.12.5
+ '@esbuild/aix-ppc64@0.28.0':
+ optional: true
+
+ '@esbuild/android-arm64@0.28.0':
+ optional: true
+
+ '@esbuild/android-arm@0.28.0':
+ optional: true
+
+ '@esbuild/android-x64@0.28.0':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.28.0':
+ optional: true
+
+ '@esbuild/darwin-x64@0.28.0':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.28.0':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.28.0':
+ optional: true
+
+ '@esbuild/linux-arm64@0.28.0':
+ optional: true
+
+ '@esbuild/linux-arm@0.28.0':
+ optional: true
+
+ '@esbuild/linux-ia32@0.28.0':
+ optional: true
+
+ '@esbuild/linux-loong64@0.28.0':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.28.0':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.28.0':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.28.0':
+ optional: true
+
+ '@esbuild/linux-s390x@0.28.0':
+ optional: true
+
+ '@esbuild/linux-x64@0.28.0':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.28.0':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.28.0':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.28.0':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.28.0':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.28.0':
+ optional: true
+
+ '@esbuild/sunos-x64@0.28.0':
+ optional: true
+
+ '@esbuild/win32-arm64@0.28.0':
+ optional: true
+
+ '@esbuild/win32-ia32@0.28.0':
+ optional: true
+
+ '@esbuild/win32-x64@0.28.0':
+ optional: true
+
'@j178/prek-darwin-arm64@0.3.10':
optional: true
@@ -1619,13 +1861,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
- '@vitest/mocker@4.1.5(vite@8.0.11(@types/node@25.6.0))':
+ '@vitest/mocker@4.1.5(vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0))':
dependencies:
'@vitest/spy': 4.1.5
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 8.0.11(@types/node@25.6.0)
+ vite: 8.0.11(@types/node@25.6.0)(esbuild@0.28.0)
'@vitest/pretty-format@4.1.5':
dependencies:
@@ -1749,6 +1991,35 @@ snapshots:
es-module-lexer@2.1.0: {}
+ esbuild@0.28.0:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.0
+ '@esbuild/android-arm': 0.28.0
+ '@esbuild/android-arm64': 0.28.0
+ '@esbuild/android-x64': 0.28.0
+ '@esbuild/darwin-arm64': 0.28.0
+ '@esbuild/darwin-x64': 0.28.0
+ '@esbuild/freebsd-arm64': 0.28.0
+ '@esbuild/freebsd-x64': 0.28.0
+ '@esbuild/linux-arm': 0.28.0
+ '@esbuild/linux-arm64': 0.28.0
+ '@esbuild/linux-ia32': 0.28.0
+ '@esbuild/linux-loong64': 0.28.0
+ '@esbuild/linux-mips64el': 0.28.0
+ '@esbuild/linux-ppc64': 0.28.0
+ '@esbuild/linux-riscv64': 0.28.0
+ '@esbuild/linux-s390x': 0.28.0
+ '@esbuild/linux-x64': 0.28.0
+ '@esbuild/netbsd-arm64': 0.28.0
+ '@esbuild/netbsd-x64': 0.28.0
+ '@esbuild/openbsd-arm64': 0.28.0
+ '@esbuild/openbsd-x64': 0.28.0
+ '@esbuild/openharmony-arm64': 0.28.0
+ '@esbuild/sunos-x64': 0.28.0
+ '@esbuild/win32-arm64': 0.28.0
+ '@esbuild/win32-ia32': 0.28.0
+ '@esbuild/win32-x64': 0.28.0
+
escalade@3.2.0: {}
escodegen@2.1.0:
@@ -2156,7 +2427,7 @@ snapshots:
undici-types@7.19.2: {}
- vite@8.0.11(@types/node@25.6.0):
+ vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -2165,12 +2436,13 @@ snapshots:
tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 25.6.0
+ esbuild: 0.28.0
fsevents: 2.3.3
- vitest@4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)):
+ vitest@4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0)):
dependencies:
'@vitest/expect': 4.1.5
- '@vitest/mocker': 4.1.5(vite@8.0.11(@types/node@25.6.0))
+ '@vitest/mocker': 4.1.5(vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0))
'@vitest/pretty-format': 4.1.5
'@vitest/runner': 4.1.5
'@vitest/snapshot': 4.1.5
@@ -2187,7 +2459,7 @@ snapshots:
tinyexec: 1.1.2
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
- vite: 8.0.11(@types/node@25.6.0)
+ vite: 8.0.11(@types/node@25.6.0)(esbuild@0.28.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.6.0
diff --git a/python/README.md b/python/README.md
index 1967245..82bb61c 100644
--- a/python/README.md
+++ b/python/README.md
@@ -150,12 +150,13 @@ Native messaging mode creates the local native host wrapper and browser manifest
Use reverse mode when the browser does not expose a public CDP websocket to the final client, but the ModCDP extension can open a websocket back to a local proxy. The proxy still serves a normal-looking CDP endpoint to Playwright, Puppeteer, Stagehand, or any other CDP client:
```sh
-pnpm run proxy -- --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --listen 127.0.0.1:9223
-pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --port 9223
+pnpm run proxy -- --upstream-mode=reversews --port 9223
+pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --port 9223
+pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29293 --port 9223
# const browser = await playwright.chromium.connectOverCDP("http://127.0.0.1:9223")
```
-Reverse mode is opt-in. The shipped extension has no generated runtime config; it always tries the fixed local reverse connector at `ws://127.0.0.1:29292`. With `--launcher-mode=local`, use that default bind and the normal `ModCDPClient` launcher, injector, and `ReverseWebSocketUpstreamTransport` path will wake the service worker and accept its connection. With `--launcher-mode=none` or a non-default bind, an already-running extension or test must explicitly call `globalThis.ModCDP.startReverseBridge("ws://host:port", { reconnect_interval_ms: 2000 })` with the chosen endpoint because there is no side channel before the reverse connection exists. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits for that extension connection. Once it connects, it self-identifies as a ModCDP extension service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker.
+Reverse mode is opt-in. The shipped extension auto-connects to the fixed local reverse connector at `ws://127.0.0.1:29292`; the proxy/client listens there and waits for that extension connection. Keep `--upstream-reversews-bind` when using a custom extension build whose compiled autoconnect URL points at a different host or port. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits. Once connected, the extension identifies itself as a ModCDP service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker.
Reverse mode is intentionally scoped to one local browser and one reverse extension connection per proxy process. The browser may still have other extensions installed; ModCDP does not require `--disable-extensions-except`.
@@ -230,7 +231,7 @@ dist/ Built JS output used by the extension and Node CLI scr
3. Attach a session to that SW target and `Runtime.enable` on it.
4. Call `globalThis.ModCDP.configure(...)` to push the resolved loopback websocket and any explicit server route overrides into the SW. The clients do this automatically by default.
-Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292` listens for the extension, while still serving downstream clients from `--listen`. The extension artifact is fixed, so automatic launch/injection uses the default `127.0.0.1:29292` connector; non-default reverse binds need an external bootstrap call to `globalThis.ModCDP.startReverseBridge(...)` with the same URL. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client.
+Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --port 9223` listens for the extension on the configured reverse connector while still serving downstream clients from the proxy port. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client.
### Send
diff --git a/python/examples/demo.py b/python/examples/demo.py
index d2ebed8..6718ce9 100644
--- a/python/examples/demo.py
+++ b/python/examples/demo.py
@@ -28,6 +28,7 @@
ROOT = Path(__file__).resolve().parent.parent.parent
EXTENSION_PATH = ROOT / "dist" / "extension"
+REVERSE_TRANSPORT_WAIT_TIMEOUT_MS = 60_000
LIVE_DEVTOOLS_ACTIVE_PORTS = [
Path.home() / "Library" / "Application Support" / "Google" / "Chrome" / "DevToolsActivePort",
Path.home() / "Library" / "Application Support" / "Google" / "Chrome Beta" / "DevToolsActivePort",
@@ -99,10 +100,17 @@ def parse_args(argv):
def client_options_for(mode, upstream_mode, cdp_url, launch_options=None):
+ upstream: ProtocolPayload = {"upstream_mode": upstream_mode, "upstream_cdp_url": cdp_url}
+ if upstream_mode == "reversews":
+ upstream["upstream_reversews_wait_timeout_ms"] = REVERSE_TRANSPORT_WAIT_TIMEOUT_MS
+ if upstream_mode == "nativemessaging":
+ upstream["upstream_nativemessaging_wait_timeout_ms"] = REVERSE_TRANSPORT_WAIT_TIMEOUT_MS
+ if upstream_mode == "nats":
+ upstream["upstream_nats_wait_timeout_ms"] = REVERSE_TRANSPORT_WAIT_TIMEOUT_MS
if mode == "direct":
return {
"launcher": {"launcher_mode": "remote" if cdp_url else "local", "launcher_options": launch_options or {}},
- "upstream": {"upstream_mode": upstream_mode, "upstream_cdp_url": cdp_url},
+ "upstream": upstream,
"injector": {"injector_mode": "auto", "injector_extension_path": str(EXTENSION_PATH)},
"client": {"client_routes": client_routes_for(mode)},
}
@@ -111,7 +119,7 @@ def client_options_for(mode, upstream_mode, cdp_url, launch_options=None):
}
return {
"launcher": {"launcher_mode": "remote" if cdp_url else "local", "launcher_options": launch_options or {}},
- "upstream": {"upstream_mode": upstream_mode, "upstream_cdp_url": cdp_url},
+ "upstream": upstream,
"injector": {"injector_mode": "auto", "injector_extension_path": str(EXTENSION_PATH)},
"client": {"client_routes": client_routes_for(mode)},
"server": server,
@@ -149,6 +157,7 @@ def main():
else:
cdp_url = None
launch_options: dict[str, object] = {
+ "chrome_ready_timeout_ms": 60_000,
"headless": sys.platform.startswith("linux") and not os.environ.get("DISPLAY"),
"sandbox": not sys.platform.startswith("linux"),
}
@@ -156,7 +165,7 @@ def main():
launch_options["executable_path"] = os.environ["CHROME_PATH"]
cdp = ModCDPClient(**client_options_for(mode, upstream_mode, cdp_url, launch_options))
- foreground_events = []
+ page_target_events = []
target_created_events = []
events_lock = threading.Lock()
@@ -165,10 +174,10 @@ def on_target_created(payload, *_):
with events_lock:
target_created_events.append(payload)
- def on_foreground_changed(payload, *_):
- print(f"Custom.foregroundTargetChanged -> {payload}")
+ def on_page_target_updated(payload, *_):
+ print(f"Custom.pageTargetUpdated -> {payload}")
with events_lock:
- foreground_events.append(payload)
+ page_target_events.append(payload)
cdp.on("Target.targetCreated", on_target_created)
@@ -323,19 +332,10 @@ def on_demo_event(payload, *_):
raise RuntimeError("expected Custom.demoEvent")
print(f"Custom.demoEvent -> {demo_event}")
- foreground_event_registration = expect_object(cdp.send("Mod.addCustomEvent", {"name": "Custom.foregroundTargetChanged"}), "Mod.addCustomEvent Custom.foregroundTargetChanged")
- if foreground_event_registration.get("registered") is not True:
- raise RuntimeError(f"unexpected foreground event registration {foreground_event_registration}")
- cdp.on("Custom.foregroundTargetChanged", on_foreground_changed)
- cdp.send("Mod.evaluate", {"expression": '''async () => {
- chrome.tabs.onActivated.addListener(async ({ tabId }) => {
- const targets = await chrome.debugger.getTargets();
- const target = targets.find(target => target.type === "page" && target.tabId === tabId);
- const tab = await chrome.tabs.get(tabId).catch(() => null);
- await cdp.emit("Custom.foregroundTargetChanged", { tabId, targetId: target?.id ?? null, url: target?.url ?? tab?.url ?? null });
- });
- return true;
- }'''})
+ page_target_event_registration = expect_object(cdp.send("Mod.addCustomEvent", {"name": "Custom.pageTargetUpdated"}), "Mod.addCustomEvent Custom.pageTargetUpdated")
+ if page_target_event_registration.get("registered") is not True:
+ raise RuntimeError(f"unexpected page target event registration {page_target_event_registration}")
+ cdp.on("Custom.pageTargetUpdated", on_page_target_updated)
cdp.send("Target.setDiscoverTargets", {"discover": True})
created_target = expect_object(cdp.send("Target.createTarget", {"url": "https://example.com", "background": True}), "Target.createTarget")
@@ -359,20 +359,32 @@ def on_demo_event(payload, *_):
print(f"Custom.TabIdFromTargetId -> {tab_from_target}")
cdp.send("Target.activateTarget", {"targetId": created_target_id})
+ page_target_emit_result = expect_object(cdp.send("Mod.evaluate", {
+ "params": {"targetId": created_target_id},
+ "expression": '''async ({ targetId }) => {
+ const targets = await chrome.debugger.getTargets();
+ const target = targets.find(target => target.id === targetId);
+ if (!target?.id) throw new Error(`target ${targetId} not found`);
+ await cdp.emit("Custom.pageTargetUpdated", { targetId: target.id, url: target.url ?? null });
+ return { emitted: true, targetId: target.id };
+ }''',
+ }), "Custom.pageTargetUpdated emit")
+ if page_target_emit_result.get("emitted") is not True or page_target_emit_result.get("targetId") != created_target_id:
+ raise RuntimeError(f"unexpected Custom.pageTargetUpdated emit result {page_target_emit_result}")
deadline = time.monotonic() + 3.0
while True:
with events_lock:
- foreground = next((event for event in foreground_events if event.get("targetId") == created_target_id), None)
- if foreground or time.monotonic() >= deadline:
+ page_target = next((event for event in page_target_events if event.get("targetId") == created_target_id), None)
+ if page_target or time.monotonic() >= deadline:
break
time.sleep(0.02)
- if not foreground:
- raise RuntimeError(f"expected Custom.foregroundTargetChanged for {created_target_id}")
- if tab_from_target.get("tabId") != foreground.get("tabId"):
- raise RuntimeError(f"unexpected Custom.foregroundTargetChanged result {foreground}")
+ if not page_target:
+ raise RuntimeError(f"expected Custom.pageTargetUpdated for {created_target_id}")
+ if tab_from_target.get("tabId") != page_target.get("tabId"):
+ raise RuntimeError(f"unexpected Custom.pageTargetUpdated result {page_target}")
- target_from_tab = expect_object(cdp.send("Custom.targetIdFromTabId", {"tabId": foreground["tabId"]}), "Custom.targetIdFromTabId")
- if target_from_tab.get("targetId") != created_target_id or target_from_tab.get("tabId") != foreground.get("tabId"):
+ target_from_tab = expect_object(cdp.send("Custom.targetIdFromTabId", {"tabId": page_target["tabId"]}), "Custom.targetIdFromTabId")
+ if target_from_tab.get("targetId") != created_target_id or target_from_tab.get("tabId") != page_target.get("tabId"):
raise RuntimeError(f"unexpected Custom.targetIdFromTabId/middleware result {target_from_tab}")
print(f"Custom.targetIdFromTabId -> {target_from_tab}")
diff --git a/python/modcdp/client/ModCDPClient.py b/python/modcdp/client/ModCDPClient.py
index 6639219..88dd819 100644
--- a/python/modcdp/client/ModCDPClient.py
+++ b/python/modcdp/client/ModCDPClient.py
@@ -13,24 +13,23 @@
import asyncio
import inspect
-import sys
import threading
import time
from collections.abc import Mapping, Sequence
from queue import Queue, Empty
-from typing import Any, cast
+from typing import Any, Literal, cast
from pydantic import BaseModel, TypeAdapter, ValidationError
from pydantic_core import to_jsonable_python
from ..router.AutoSessionRouter import AutoSessionRouter
from ..types.jsonschema import type_adapter_from_json_schema
-from ..types.generated.cdp import AwaitableDict, CDPEvent, CDPSurfaceMixin, cdp_event_name, install_cdp_surface
+from ..types.generated import cdp as generated_cdp
+from ..types.generated.cdp import AwaitableDict, CDPEvent, CDPModel, CDPParams, CDPSurfaceMixin, cdp_event_name, install_cdp_surface
from ..launcher.BrowserbaseBrowserLauncher import BrowserbaseBrowserLauncher
from ..injector.BBBrowserExtensionInjector import BBBrowserExtensionInjector
from ..injector.BorrowedExtensionInjector import BorrowedExtensionInjector
from ..injector.DiscoveredExtensionInjector import DiscoveredExtensionInjector
from ..injector.ExtensionInjector import (
- DEFAULT_MODCDP_WAKE_PATH,
DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES,
ExtensionInjector,
ExtensionInjectorConfig,
@@ -120,7 +119,13 @@ class _ModDomain:
def __init__(self, client: "ModCDPClient") -> None:
self._client = client
- def evaluate(self, *, expression: str, params: Mapping[str, Any] | None = None, cdpSessionId: str | None = None):
+ def evaluate(
+ self,
+ *,
+ expression: str,
+ params: Mapping[str, Any] | None = None,
+ cdpSessionId: str | None = None,
+ ) -> AwaitableDict | AwaitableValue:
payload: dict[str, Any] = {"expression": expression}
if params is not None:
payload["params"] = dict(params)
@@ -135,7 +140,7 @@ def addCustomCommand(
params_schema: Any | None = None,
result_schema: Any | None = None,
expression: str | None = None,
- ):
+ ) -> AwaitableDict | AwaitableValue:
payload: dict[str, Any] = {"name": name}
if params_schema is not None:
payload["params_schema"] = params_schema
@@ -145,22 +150,28 @@ def addCustomCommand(
payload["expression"] = expression
return self._client._send_command("Mod.addCustomCommand", payload)
- def addCustomEvent(self, name: str, *, event_schema: Any | None = None):
+ def addCustomEvent(self, name: str, *, event_schema: Any | None = None) -> AwaitableDict | AwaitableValue:
payload: dict[str, Any] = {"name": name}
if event_schema is not None:
payload["event_schema"] = event_schema
return self._client._send_command("Mod.addCustomEvent", payload)
- def addMiddleware(self, *, phase: str, expression: str, name: str | None = None):
+ def addMiddleware(
+ self,
+ *,
+ phase: Literal["request", "response", "event"],
+ expression: str,
+ name: str | None = None,
+ ) -> AwaitableDict | AwaitableValue:
payload: dict[str, Any] = {"phase": phase, "expression": expression}
if name is not None:
payload["name"] = name
return self._client._send_command("Mod.addMiddleware", payload)
- def configure(self, **params: Any):
+ def configure(self, **params: Any) -> AwaitableDict | AwaitableValue:
return self._client._send_command("Mod.configure", params)
- def ping(self, **params: Any):
+ def ping(self, **params: Any) -> AwaitableDict | AwaitableValue:
return self._client._send_command("Mod.ping", params)
MODCDP_READY_EXPRESSION = (
@@ -265,8 +276,6 @@ def __init__(
"injector_mode": injector_mode,
"injector_extension_path": injector_input.get("injector_extension_path"),
"injector_extension_id": injector_input.get("injector_extension_id"),
- "injector_wake_path": _defaulted(injector_input.get("injector_wake_path"), DEFAULT_MODCDP_WAKE_PATH),
- "injector_wake_url": injector_input.get("injector_wake_url"),
"injector_service_worker_url_includes": list(cast(Sequence[str], injector_input.get("injector_service_worker_url_includes") or [])),
"injector_service_worker_url_suffixes": list(
cast(
@@ -354,6 +363,7 @@ def __init__(
self._launched_browser: Any | None = None
self._extension_injectors: list[ExtensionInjector] = []
self._cdp = _RawCDP(self)
+ self._hydrate_native_protocol_schemas()
self._hydrate_custom_surface()
def connect(self) -> "ModCDPClient":
@@ -592,7 +602,6 @@ def __getattr__(self, domain: str):
def _server_configure_params(self) -> ModCDPServerConfig:
server = dict(self.server or {})
- transport_injector_config = self.transport.getInjectorConfig() if self.transport is not None else {}
server_routes = server.pop("server_routes", None)
server_loopback_cdp_url = server.pop("server_loopback_cdp_url", None)
server_browser_token = server.pop("server_browser_token", None)
@@ -624,11 +633,6 @@ def _server_configure_params(self) -> ModCDPServerConfig:
return cast(ModCDPServerConfig, {
"upstream": {
"upstream_mode": self.upstream.get("upstream_mode"),
- **(
- {"upstream_reversews_url": transport_injector_config.get("upstream_reversews_url")}
- if transport_injector_config.get("upstream_reversews_url")
- else {}
- ),
**({"upstream_nats_url": self.upstream.get("upstream_nats_url")} if self.upstream.get("upstream_nats_url") else {}),
**(
{"upstream_nats_subject_prefix": self.upstream.get("upstream_nats_subject_prefix")}
@@ -823,7 +827,8 @@ def _extension_injectors_for_config(self) -> list[ExtensionInjector]:
if mode == "none":
return []
injectors: list[ExtensionInjector] = []
- if mode in ("auto", "discover"):
+ prefer_launch_injection = mode == "auto" and self.launcher.get("launcher_mode") == "local"
+ if mode in ("auto", "discover") and not prefer_launch_injection:
injectors.append(DiscoveredExtensionInjector())
if mode in ("auto", "inject"):
if self.launcher.get("launcher_mode") == "bb":
@@ -831,6 +836,8 @@ def _extension_injectors_for_config(self) -> list[ExtensionInjector]:
if self.launcher.get("launcher_mode") == "local":
injectors.append(LocalBrowserLaunchExtensionInjector())
injectors.append(ExtensionsLoadUnpackedInjector())
+ if prefer_launch_injection:
+ injectors.append(DiscoveredExtensionInjector())
if mode in ("auto", "borrow"):
injectors.append(BorrowedExtensionInjector())
if not injectors:
@@ -871,8 +878,6 @@ def attach_to_target(target_id: str) -> str | None:
"waitForExecutionContext": self.auto_sessions.waitForExecutionContext,
"injector_extension_path": cast(str | None, self.injector.get("injector_extension_path")),
"injector_extension_id": cast(str | None, self.injector.get("injector_extension_id")),
- "injector_wake_path": cast(str | None, self.injector.get("injector_wake_path")),
- "injector_wake_url": cast(str | None, self.injector.get("injector_wake_url")),
"injector_service_worker_url_includes": cast(list[str], self.injector["injector_service_worker_url_includes"]),
"injector_service_worker_url_suffixes": cast(list[str], self.injector["injector_service_worker_url_suffixes"]),
"injector_trust_service_worker_target": trust_service_worker_target,
@@ -934,6 +939,35 @@ def _hydrate_custom_surface(self) -> None:
continue
self._register_custom_event(cast(ProtocolParams, event))
+ def _hydrate_native_protocol_schemas(self) -> None:
+ with self._schema_lock:
+ for domain_name, domain_class in vars(generated_cdp).items():
+ if not domain_name.endswith("Domain") or not isinstance(domain_class, type):
+ continue
+ domain = domain_name.removesuffix("Domain")
+ nested_classes = {
+ name: value
+ for name, value in vars(domain_class).items()
+ if isinstance(value, type) and issubclass(value, CDPModel)
+ }
+ for class_name, params_class in nested_classes.items():
+ if issubclass(params_class, CDPEvent):
+ event_name = getattr(params_class, "cdp_event_name", None)
+ if isinstance(event_name, str):
+ self._event_schemas[event_name] = TypeAdapter(params_class)
+ continue
+ if not class_name.startswith("_") or not class_name.endswith("Params"):
+ continue
+ command_base = class_name[1:-6]
+ result_class = nested_classes.get(f"_{command_base}Result")
+ if result_class is None:
+ continue
+ method = f"{domain}.{command_base[:1].lower()}{command_base[1:]}"
+ if issubclass(params_class, CDPParams):
+ self._command_params_schemas[method] = TypeAdapter(params_class)
+ self._command_result_schemas[method] = TypeAdapter(result_class)
+ self._command_result_model_schemas.add(method)
+
def _register_custom_command(self, params: ProtocolParams) -> None:
name = params.get("name")
if not isinstance(name, str) or not name:
@@ -996,10 +1030,14 @@ def _validate_command_params(self, method: str, params: ProtocolParams) -> Proto
if adapter is None:
return params
try:
- validated = adapter.validate_python(dict(params))
+ validated = adapter.validate_python(dict(params), strict=True)
except ValidationError as e:
raise ValueError(f"{method} params did not match params_schema: {e}") from e
- jsonable = to_jsonable_python(validated)
+ jsonable = (
+ validated.model_dump(mode="json", exclude_none=True, by_alias=True)
+ if isinstance(validated, BaseModel)
+ else to_jsonable_python(validated)
+ )
if not isinstance(jsonable, Mapping):
raise ValueError(f"{method} params_schema must validate to a JSON object")
return cast(ProtocolParams, dict(jsonable))
@@ -1010,9 +1048,11 @@ def _validate_command_result(self, method: str, result: Any) -> Any:
if adapter is None:
return result
try:
- validated = adapter.validate_python(result)
+ validated = adapter.validate_python(result, strict=True)
except ValidationError as e:
raise ValueError(f"{method} result did not match result_schema: {e}") from e
+ if isinstance(validated, CDPModel):
+ return cast(JsonValue, validated.model_dump(mode="json", exclude_none=True, by_alias=True))
if method in self._command_result_model_schemas and isinstance(validated, BaseModel):
fields = list(type(validated).model_fields)
if len(fields) == 1:
@@ -1023,22 +1063,29 @@ def _validate_command_result(self, method: str, result: Any) -> Any:
def _validate_event_payload(self, event: str, payload: ProtocolPayload) -> Any | None:
with self._schema_lock:
adapter = self._event_schemas.get(event)
- if adapter is None:
+ event_class = self._event_classes.get(event)
+ if adapter is None and event_class is None:
return dict(payload)
+ if adapter is None and event_class is not None:
+ try:
+ return cast(ProtocolPayload, event_class.model_validate(dict(payload)).model_dump(mode="json", exclude_none=True, by_alias=True))
+ except ValidationError as e:
+ raise ValueError(f"{event} event did not match native event schema: {e}") from e
+ assert adapter is not None
try:
- validated = adapter.validate_python(dict(payload))
+ validated = adapter.validate_python(dict(payload), strict=True)
except ValidationError as direct_error:
if set(payload.keys()) != {"value"}:
- print(f"[ModCDPClient] event {event} did not match event_schema: {direct_error}", file=sys.stderr)
- return None
+ raise ValueError(f"{event} event did not match event_schema: {direct_error}") from direct_error
try:
- validated = adapter.validate_python(payload["value"])
+ validated = adapter.validate_python(payload["value"], strict=True)
except ValidationError as value_error:
- print(f"[ModCDPClient] event {event} did not match event_schema: {value_error}", file=sys.stderr)
- return None
+ raise ValueError(f"{event} event did not match event_schema: {value_error}") from value_error
if event in self._event_model_schemas:
return cast(ProtocolPayload, validated)
return {"value": cast(JsonValue, to_jsonable_python(validated))}
+ if isinstance(validated, CDPModel):
+ return cast(ProtocolPayload, validated.model_dump(mode="json", exclude_none=True, by_alias=True))
if event in self._event_model_schemas:
return cast(ProtocolPayload, validated)
jsonable = to_jsonable_python(validated)
diff --git a/python/modcdp/extension.zip b/python/modcdp/extension.zip
index 268e970..2f87949 100644
Binary files a/python/modcdp/extension.zip and b/python/modcdp/extension.zip differ
diff --git a/python/modcdp/injector/BorrowedExtensionInjector.py b/python/modcdp/injector/BorrowedExtensionInjector.py
index e3f6bcd..326b624 100644
--- a/python/modcdp/injector/BorrowedExtensionInjector.py
+++ b/python/modcdp/injector/BorrowedExtensionInjector.py
@@ -117,6 +117,7 @@ def bootstrap_modcdp_server_expression() -> str:
def modcdp_server_source() -> str:
candidates: list[Path] = []
for parent in Path(__file__).resolve().parents:
+ candidates.append(parent / "dist" / "js" / "src" / "server" / "ModCDPServer.js")
candidates.append(parent / "dist" / "extension" / "js" / "src" / "server" / "ModCDPServer.js")
candidates.append(parent / "dist" / "extension" / "ModCDPServer.js")
candidates.append(parent / "ModCDPServer.js")
diff --git a/python/modcdp/injector/DiscoveredExtensionInjector.py b/python/modcdp/injector/DiscoveredExtensionInjector.py
index 6e5e914..7a550f4 100644
--- a/python/modcdp/injector/DiscoveredExtensionInjector.py
+++ b/python/modcdp/injector/DiscoveredExtensionInjector.py
@@ -21,14 +21,6 @@ def inject(self) -> ExtensionInjectionResult | None:
)
if waited:
return {**waited, "source": "discovered"}
- woke = self._wakeConfiguredExtension()
- if woke:
- waited = self._waitForReadyServiceWorker(
- _timeout(self.options.get("injector_service_worker_probe_timeout_ms"), DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS),
- matched_only=bool(self.options.get("injector_trust_service_worker_target")),
- )
- if waited:
- return {**waited, "source": "discovered"}
if not self.options.get("injector_require_service_worker_target"):
return None
waited = self._waitForReadyServiceWorker(
diff --git a/python/modcdp/injector/ExtensionInjector.py b/python/modcdp/injector/ExtensionInjector.py
index 48b0ba7..510abb3 100644
--- a/python/modcdp/injector/ExtensionInjector.py
+++ b/python/modcdp/injector/ExtensionInjector.py
@@ -1,8 +1,14 @@
from __future__ import annotations
+import base64
+import hashlib
+import json
import re
+import shutil
+import tempfile
import threading
import time
+import zipfile
from collections.abc import Callable, Mapping
from pathlib import Path
from queue import Empty, Queue
@@ -16,7 +22,6 @@
EXT_ID_FROM_URL_RE = re.compile(r"^chrome-extension://([a-z]+)/")
DEFAULT_MODCDP_EXTENSION_ID = "mdedooklbnfejodmnhmkdpkaedafkehf"
DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES = ["/modcdp/service_worker.js"]
-DEFAULT_MODCDP_WAKE_PATH = "/modcdp/wake.html"
MODCDP_READY_EXPRESSION = (
"Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)"
)
@@ -40,8 +45,6 @@ class ExtensionInjectorConfig(TypedDict, total=False):
waitForExecutionContext: WaitForExecutionContext | None
injector_extension_path: str | None
injector_extension_id: str | None
- injector_wake_path: str | None
- injector_wake_url: str | None
injector_service_worker_url_includes: list[str]
injector_service_worker_url_suffixes: list[str]
injector_trust_service_worker_target: bool
@@ -55,7 +58,6 @@ class ExtensionInjectorConfig(TypedDict, total=False):
injector_target_session_poll_interval_ms: int
injector_browserbase_api_key: str | None
injector_browserbase_base_url: str | None
- upstream_reversews_url: str | None
upstream_nativemessaging_host_name: str | None
upstream_nats_url: str | None
upstream_nats_subject_prefix: str | None
@@ -66,6 +68,51 @@ def defaultModCDPExtensionPath() -> str | None:
return str(bundled_extension) if bundled_extension.exists() else None
+def prepareUnpackedExtension(extension_path: str) -> tuple[str, tempfile.TemporaryDirectory[str]]:
+ cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-")
+ try:
+ if extension_path.endswith(".zip"):
+ with zipfile.ZipFile(extension_path) as archive:
+ _extract_zip(archive, cleanup_dir.name)
+ else:
+ shutil.copytree(extension_path, cleanup_dir.name, dirs_exist_ok=True)
+ return _extension_root(cleanup_dir.name), cleanup_dir
+ except BaseException:
+ cleanup_dir.cleanup()
+ raise
+
+
+def extensionIdFromManifestKey(extension_path: str) -> str | None:
+ manifest_path = Path(extension_path) / "manifest.json"
+ if not manifest_path.exists():
+ return None
+ manifest = json.loads(manifest_path.read_text())
+ key = manifest.get("key") if isinstance(manifest, dict) else None
+ if not isinstance(key, str) or not key.strip():
+ return None
+ digest = hashlib.sha256(base64.b64decode(key)).digest()[:16]
+ alphabet = "abcdefghijklmnop"
+ return "".join(alphabet[byte >> 4] + alphabet[byte & 0x0F] for byte in digest)
+
+
+def _extension_root(unpacked_path: str) -> str:
+ if (Path(unpacked_path) / "manifest.json").exists():
+ return unpacked_path
+ nested = Path(unpacked_path) / "extension"
+ if (nested / "manifest.json").exists():
+ return str(nested)
+ return unpacked_path
+
+
+def _extract_zip(archive: zipfile.ZipFile, destination: str) -> None:
+ root = Path(destination).resolve()
+ for member in archive.infolist():
+ target = (root / member.filename).resolve()
+ if target != root and root not in target.parents:
+ raise RuntimeError(f'zip entry "{member.filename}" escapes extension extraction directory')
+ archive.extractall(destination)
+
+
def _defaulted(value: Any, fallback: int) -> int:
return fallback if value is None else int(value)
@@ -89,8 +136,6 @@ def __init__(self, options: ExtensionInjectorConfig | None = None) -> None:
"waitForExecutionContext": None,
"injector_extension_path": None,
"injector_extension_id": None,
- "injector_wake_path": DEFAULT_MODCDP_WAKE_PATH,
- "injector_wake_url": None,
"injector_service_worker_url_includes": [],
"injector_service_worker_url_suffixes": [],
"injector_trust_service_worker_target": False,
@@ -104,7 +149,6 @@ def __init__(self, options: ExtensionInjectorConfig | None = None) -> None:
"injector_target_session_poll_interval_ms": DEFAULT_TARGET_SESSION_POLL_INTERVAL_MS,
"injector_browserbase_api_key": None,
"injector_browserbase_base_url": None,
- "upstream_reversews_url": None,
"upstream_nativemessaging_host_name": None,
"upstream_nats_url": None,
"upstream_nats_subject_prefix": None,
@@ -233,35 +277,6 @@ def _targetInfos(self) -> list[TargetInfo]:
targets.append({"targetId": target_id, "type": target_type, "url": target_url})
return targets
- def _configuredWakeUrl(self) -> str | None:
- wake_url = self.options.get("injector_wake_url")
- if wake_url:
- return wake_url
- extension_id = self.options.get("injector_extension_id")
- if not extension_id:
- return None
- wake_path = self.options.get("injector_wake_path")
- wake_path = DEFAULT_MODCDP_WAKE_PATH if wake_path is None else wake_path
- return f"chrome-extension://{extension_id}{wake_path if wake_path.startswith('/') else f'/{wake_path}'}"
-
- def _wakeConfiguredExtension(self) -> bool:
- wake_url = self._configuredWakeUrl()
- if not wake_url or self.options.get("send") is None:
- return False
- try:
- self._sendWithTimeout(
- "Target.createTarget",
- {
- "url": wake_url,
- "background": True,
- "hidden": True,
- "focus": False,
- },
- )
- return True
- except Exception:
- return False
-
def _probeTarget(
self,
target: TargetInfo,
diff --git a/python/modcdp/injector/ExtensionsLoadUnpackedInjector.py b/python/modcdp/injector/ExtensionsLoadUnpackedInjector.py
index 4e72da6..9e4d7d3 100644
--- a/python/modcdp/injector/ExtensionsLoadUnpackedInjector.py
+++ b/python/modcdp/injector/ExtensionsLoadUnpackedInjector.py
@@ -2,11 +2,8 @@
import tempfile
import time
-import zipfile
-import shutil
-from pathlib import Path
-from ..injector.ExtensionInjector import DEFAULT_SERVICE_WORKER_POLL_INTERVAL_MS, DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS, DEFAULT_SERVICE_WORKER_READY_TIMEOUT_MS, ExtensionInjector, ExtensionInjectionResult, defaultModCDPExtensionPath
+from ..injector.ExtensionInjector import DEFAULT_SERVICE_WORKER_POLL_INTERVAL_MS, DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS, DEFAULT_SERVICE_WORKER_READY_TIMEOUT_MS, ExtensionInjector, ExtensionInjectionResult, defaultModCDPExtensionPath, prepareUnpackedExtension
class ExtensionsLoadUnpackedInjector(ExtensionInjector):
@@ -21,16 +18,7 @@ def prepare(self) -> None:
super().prepare()
return
self.options["injector_extension_path"] = extension_path
- if not extension_path.endswith(".zip"):
- self.cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-")
- shutil.copytree(extension_path, self.cleanup_dir.name, dirs_exist_ok=True)
- self.unpacked_extension_path = _extension_root(self.cleanup_dir.name)
- super().prepare()
- return
- self.cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-")
- with zipfile.ZipFile(extension_path) as archive:
- archive.extractall(self.cleanup_dir.name)
- self.unpacked_extension_path = _extension_root(self.cleanup_dir.name)
+ self.unpacked_extension_path, self.cleanup_dir = prepareUnpackedExtension(extension_path)
super().prepare()
def inject(self) -> ExtensionInjectionResult | None:
@@ -51,7 +39,6 @@ def inject(self) -> ExtensionInjectionResult | None:
if not isinstance(extension_id, str) or not extension_id:
raise RuntimeError(f"Extensions.loadUnpacked returned no extension id (got {load_result})")
self.options["injector_extension_id"] = extension_id
- self._wakeConfiguredExtension()
sw_url_prefix = f"chrome-extension://{extension_id}/"
deadline = time.monotonic() + (self.options.get("injector_service_worker_ready_timeout_ms") or DEFAULT_SERVICE_WORKER_READY_TIMEOUT_MS) / 1000
@@ -74,12 +61,3 @@ def close(self) -> None:
if self.cleanup_dir:
self.cleanup_dir.cleanup()
self.cleanup_dir = None
-
-
-def _extension_root(unpacked_path: str) -> str:
- if (Path(unpacked_path) / "manifest.json").exists():
- return unpacked_path
- nested = Path(unpacked_path) / "extension"
- if (nested / "manifest.json").exists():
- return str(nested)
- return unpacked_path
diff --git a/python/modcdp/injector/LocalBrowserLaunchExtensionInjector.py b/python/modcdp/injector/LocalBrowserLaunchExtensionInjector.py
index c3a47f0..79e51dc 100644
--- a/python/modcdp/injector/LocalBrowserLaunchExtensionInjector.py
+++ b/python/modcdp/injector/LocalBrowserLaunchExtensionInjector.py
@@ -1,15 +1,15 @@
from __future__ import annotations
-import base64
-import hashlib
-import json
-import shutil
import tempfile
-import zipfile
-from pathlib import Path
from ..launcher.BrowserLauncher import BrowserLaunchOptions
-from ..injector.ExtensionInjector import DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS, ExtensionInjector, ExtensionInjectionResult, defaultModCDPExtensionPath
+from ..injector.ExtensionInjector import (
+ ExtensionInjector,
+ ExtensionInjectionResult,
+ defaultModCDPExtensionPath,
+ extensionIdFromManifestKey,
+ prepareUnpackedExtension,
+)
class LocalBrowserLaunchExtensionInjector(ExtensionInjector):
@@ -25,17 +25,7 @@ def prepare(self) -> None:
super().prepare()
return
self.options["injector_extension_path"] = extension_path
- if not extension_path.endswith(".zip"):
- self.cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-")
- shutil.copytree(extension_path, self.cleanup_dir.name, dirs_exist_ok=True)
- self.unpacked_extension_path = _extension_root(self.cleanup_dir.name)
- self._resolveExtensionId()
- super().prepare()
- return
- self.cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-")
- with zipfile.ZipFile(extension_path) as archive:
- archive.extractall(self.cleanup_dir.name)
- self.unpacked_extension_path = _extension_root(self.cleanup_dir.name)
+ self.unpacked_extension_path, self.cleanup_dir = prepareUnpackedExtension(extension_path)
self._resolveExtensionId()
super().prepare()
@@ -45,9 +35,7 @@ def getLauncherConfig(self) -> BrowserLaunchOptions:
return {"extra_args": [f"--load-extension={self.unpacked_extension_path}"]}
def inject(self) -> ExtensionInjectionResult | None:
- timeout_ms = self.options.get("injector_service_worker_probe_timeout_ms") or DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS
- discovered = self._waitForReadyServiceWorker(
- timeout_ms,
+ discovered = self._discoverReadyServiceWorker(
matched_only=bool(self.options.get("injector_trust_service_worker_target")),
)
return {**discovered, "source": "local_launch"} if discovered else None
@@ -69,25 +57,3 @@ def _resolveExtensionId(self) -> str | None:
if self.extension_id:
self.options["injector_extension_id"] = self.extension_id
return self.extension_id
-
-
-def extensionIdFromManifestKey(extension_path: str) -> str | None:
- manifest_path = Path(extension_path) / "manifest.json"
- if not manifest_path.exists():
- return None
- manifest = json.loads(manifest_path.read_text())
- key = manifest.get("key") if isinstance(manifest, dict) else None
- if not isinstance(key, str) or not key.strip():
- return None
- digest = hashlib.sha256(base64.b64decode(key)).digest()[:16]
- alphabet = "abcdefghijklmnop"
- return "".join(alphabet[byte >> 4] + alphabet[byte & 0x0F] for byte in digest)
-
-
-def _extension_root(unpacked_path: str) -> str:
- if (Path(unpacked_path) / "manifest.json").exists():
- return unpacked_path
- nested = Path(unpacked_path) / "extension"
- if (nested / "manifest.json").exists():
- return str(nested)
- return unpacked_path
diff --git a/python/modcdp/launcher/LocalBrowserLauncher.py b/python/modcdp/launcher/LocalBrowserLauncher.py
index cce641d..8f5569e 100644
--- a/python/modcdp/launcher/LocalBrowserLauncher.py
+++ b/python/modcdp/launcher/LocalBrowserLauncher.py
@@ -1,7 +1,7 @@
from __future__ import annotations
-import json
import glob
+import json
import os
import re
import select
@@ -43,7 +43,8 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser
executable_path = self.findChromeBinary(merged.get("executable_path"))
use_pipe = merged.get("remote_debugging") == "pipe"
use_loopback_cdp = (not use_pipe) or bool(merged.get("loopback_cdp")) or merged.get("port") is not None
- port = int(merged.get("port") or LocalBrowserLauncher.freePort()) if use_loopback_cdp else None
+ requested_port = merged.get("port")
+ port = int(requested_port) if use_loopback_cdp and requested_port is not None else (0 if use_loopback_cdp else None)
temp_profile_dir: tempfile.TemporaryDirectory[str] | None = None
profile_dir = merged.get("user_data_dir")
if not profile_dir:
@@ -75,7 +76,8 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser
default_headless = sys.platform.startswith("linux") and not os.environ.get("DISPLAY")
if merged.get("headless", default_headless):
args.append("--headless=new")
- if merged.get("sandbox", False) is False:
+ default_sandbox = not sys.platform.startswith("linux")
+ if merged.get("sandbox", default_sandbox) is False:
args.append("--no-sandbox")
args.extend(list(merged.get("args") or []))
args.extend(list(merged.get("extra_args") or []))
@@ -85,6 +87,8 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser
child_read, parent_write = os.pipe()
parent_read = _move_fd_if_needed(parent_read, {3, 4})
parent_write = _move_fd_if_needed(parent_write, {3, 4})
+ child_read = _move_fd_if_needed(child_read, {3, 4})
+ child_write = _move_fd_if_needed(child_write, {3, 4})
process = _spawn_chrome_with_pipe_fds(executable_path, args, child_read, child_write)
os.close(child_read)
os.close(child_write)
@@ -93,10 +97,19 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser
try:
_wait_for_pipe_ready(pipe_read, pipe_write, int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS))
loopback_cdp_url = (
- _wait_for_cdp_websocket_url(
- f"http://127.0.0.1:{port}",
- int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS),
- int(merged.get("chrome_ready_poll_interval_ms") or DEFAULT_CHROME_READY_POLL_INTERVAL_MS),
+ (
+ _wait_for_browser_selected_cdp_websocket_url(
+ str(profile_dir),
+ int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS),
+ int(merged.get("chrome_ready_poll_interval_ms") or DEFAULT_CHROME_READY_POLL_INTERVAL_MS),
+ process,
+ )
+ if port == 0
+ else _wait_for_cdp_websocket_url(
+ f"http://127.0.0.1:{port}",
+ int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS),
+ int(merged.get("chrome_ready_poll_interval_ms") or DEFAULT_CHROME_READY_POLL_INTERVAL_MS),
+ )
)
if port is not None
else None
@@ -130,11 +143,22 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser
stderr=subprocess.DEVNULL,
start_new_session=not sys.platform.startswith("win"),
)
- cdp_url = f"http://127.0.0.1:{port}"
timeout_s = int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS) / 1000
poll_s = int(merged.get("chrome_ready_poll_interval_ms") or DEFAULT_CHROME_READY_POLL_INTERVAL_MS) / 1000
deadline = time.time() + timeout_s
while time.time() < deadline:
+ exit_code = process.poll()
+ if exit_code is not None:
+ _close(process, temp_profile_dir, cleanup_profile_dir=cleanup_profile_dir)
+ raise RuntimeError(f"Chrome exited before CDP became ready (exit={exit_code}).")
+ if port == 0:
+ active_port = _read_devtools_active_port(str(profile_dir))
+ if active_port is None:
+ time.sleep(poll_s)
+ continue
+ cdp_url = f"http://127.0.0.1:{active_port}"
+ else:
+ cdp_url = f"http://127.0.0.1:{port}"
try:
with urllib.request.urlopen(f"{cdp_url}/json/version", timeout=0.5) as response:
version = json.loads(response.read())
@@ -149,7 +173,7 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser
except Exception:
time.sleep(poll_s)
_close(process, temp_profile_dir, cleanup_profile_dir=cleanup_profile_dir)
- raise RuntimeError(f"Chrome at {cdp_url} did not become ready within {timeout_s}s")
+ raise RuntimeError(f"Chrome did not become ready within {timeout_s}s")
def _newest_first(candidates: list[str]) -> list[str]:
@@ -205,77 +229,41 @@ def _candidate_paths() -> list[str]:
str(local_app_data / "Google/Chrome/Application/chrome.exe"),
]
else:
+ chromium = ["/usr/bin/chromium", "/usr/bin/chromium-browser"]
canary = ["/usr/bin/google-chrome-canary", "/usr/bin/google-chrome-unstable", "/opt/google/chrome-unstable/chrome"]
stock = ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/opt/google/chrome/chrome"]
- return [*_chrome_for_testing_candidates(), *canary, *stock]
+ return [*chromium, *canary, *_chrome_for_testing_candidates(), *stock]
+ return [*canary, *_chrome_for_testing_candidates(), *stock]
def _move_fd_if_needed(fd: int, reserved: set[int]) -> int:
if fd not in reserved:
return fd
moved = os.dup(fd)
+ while moved in reserved:
+ next_fd = os.dup(fd)
+ os.close(moved)
+ moved = next_fd
os.close(fd)
return moved
-class _SpawnedProcess:
- def __init__(self, pid: int) -> None:
- self.pid = pid
- self.returncode: int | None = None
+def _spawn_chrome_with_pipe_fds(executable_path: str, args: list[str], child_read: int, child_write: int) -> _ChromeProcess:
+ def map_pipe_fds() -> None:
+ os.dup2(child_read, 3)
+ os.dup2(child_write, 4)
+ os.close(child_read)
+ os.close(child_write)
- def terminate(self) -> None:
- self._signal(signal.SIGTERM)
-
- def kill(self) -> None:
- self._signal(signal.SIGKILL)
-
- def wait(self, timeout: float | None = None) -> int:
- deadline = None if timeout is None else time.monotonic() + timeout
- while True:
- try:
- waited_pid, status = os.waitpid(self.pid, os.WNOHANG)
- except ChildProcessError:
- if self.returncode is None:
- self.returncode = 0
- return self.returncode
- if waited_pid == self.pid:
- self.returncode = os.waitstatus_to_exitcode(status)
- return self.returncode
- if deadline is not None and time.monotonic() >= deadline:
- assert timeout is not None
- raise subprocess.TimeoutExpired([str(self.pid)], timeout)
- time.sleep(0.05)
-
- def _signal(self, sig: int) -> None:
- if self.returncode is not None:
- return
- try:
- os.kill(self.pid, sig)
- except ProcessLookupError:
- self.returncode = 0
-
-
-def _spawn_chrome_with_pipe_fds(executable_path: str, args: list[str], child_read: int, child_write: int) -> _SpawnedProcess:
- if not hasattr(os, "posix_spawn"):
- raise RuntimeError("remote_debugging='pipe' requires os.posix_spawn support in the Python client.")
- os.set_inheritable(child_read, True)
- os.set_inheritable(child_write, True)
- devnull = os.open(os.devnull, os.O_RDWR)
- os.set_inheritable(devnull, True)
- file_actions: list[tuple[int, int] | tuple[int, int, int]] = [
- (os.POSIX_SPAWN_DUP2, devnull, 0),
- (os.POSIX_SPAWN_DUP2, devnull, 1),
- (os.POSIX_SPAWN_DUP2, devnull, 2),
- (os.POSIX_SPAWN_DUP2, child_read, 3),
- (os.POSIX_SPAWN_DUP2, child_write, 4),
- ]
- for fd in {devnull, child_read, child_write} - {0, 1, 2, 3, 4}:
- file_actions.append((os.POSIX_SPAWN_CLOSE, fd))
- try:
- pid = os.posix_spawn(executable_path, [executable_path, *args], os.environ, file_actions=file_actions)
- return _SpawnedProcess(pid)
- finally:
- os.close(devnull)
+ return subprocess.Popen(
+ [executable_path, *args],
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ close_fds=sys.platform.startswith("win"),
+ preexec_fn=None if sys.platform.startswith("win") else map_pipe_fds,
+ start_new_session=not sys.platform.startswith("win"),
+ )
class _ChromeProcess(Protocol):
@@ -287,6 +275,8 @@ def kill(self) -> None: ...
def wait(self, timeout: float | None = None) -> int | None: ...
+ def poll(self) -> int | None: ...
+
def _wait_for_pipe_ready(pipe_read, pipe_write, timeout_ms: int) -> None:
ready_id = 1
@@ -320,6 +310,7 @@ def _wait_for_pipe_ready(pipe_read, pipe_write, timeout_ms: int) -> None:
def _wait_for_cdp_websocket_url(cdp_url: str, timeout_ms: int, poll_interval_ms: int) -> str:
deadline = time.time() + timeout_ms / 1000
poll_s = poll_interval_ms / 1000
+ last_error: Exception | None = None
while time.time() < deadline:
try:
with urllib.request.urlopen(f"{cdp_url}/json/version", timeout=0.5) as response:
@@ -327,12 +318,55 @@ def _wait_for_cdp_websocket_url(cdp_url: str, timeout_ms: int, poll_interval_ms:
websocket_url = version.get("webSocketDebuggerUrl")
if websocket_url:
return str(websocket_url)
- except Exception:
- pass
+ except Exception as err:
+ last_error = err
time.sleep(poll_s)
+ if last_error is not None:
+ raise RuntimeError(f"Chrome at {cdp_url} did not expose a WebSocket CDP URL within {timeout_ms}ms: {last_error}")
raise RuntimeError(f"Chrome at {cdp_url} did not expose a WebSocket CDP URL within {timeout_ms}ms")
+def _read_devtools_active_port(profile_dir: str) -> int | None:
+ active_port_path = Path(profile_dir) / "DevToolsActivePort"
+ try:
+ raw_port, websocket_path, *_ = active_port_path.read_text().strip().splitlines()
+ except FileNotFoundError:
+ return None
+ except ValueError:
+ return None
+ if not websocket_path:
+ return None
+ port = int(raw_port)
+ if port <= 0:
+ raise RuntimeError(f"Invalid DevToolsActivePort port: {raw_port}")
+ return port
+
+
+def _wait_for_browser_selected_cdp_websocket_url(
+ profile_dir: str,
+ timeout_ms: int,
+ poll_interval_ms: int,
+ process: _ChromeProcess,
+) -> str:
+ deadline = time.time() + timeout_ms / 1000
+ poll_s = poll_interval_ms / 1000
+ last_error: Exception | None = None
+ while time.time() < deadline:
+ exit_code = process.poll()
+ if exit_code is not None:
+ raise RuntimeError(f"Chrome exited before CDP became ready (exit={exit_code}).")
+ active_port = _read_devtools_active_port(profile_dir)
+ if active_port is not None:
+ try:
+ return _wait_for_cdp_websocket_url(f"http://127.0.0.1:{active_port}", poll_interval_ms, poll_interval_ms)
+ except Exception as err:
+ last_error = err
+ time.sleep(poll_s)
+ if last_error is not None:
+ raise RuntimeError(f"Chrome did not expose DevToolsActivePort from {profile_dir} within {timeout_ms}ms: {last_error}")
+ raise RuntimeError(f"Chrome did not expose DevToolsActivePort from {profile_dir} within {timeout_ms}ms")
+
+
def _close(
process: _ChromeProcess,
temp_profile_dir: tempfile.TemporaryDirectory[str] | None,
diff --git a/python/modcdp/translate/translate.py b/python/modcdp/translate/translate.py
index 06bbde5..648ba16 100644
--- a/python/modcdp/translate/translate.py
+++ b/python/modcdp/translate/translate.py
@@ -152,10 +152,13 @@ def _wrap_modcdp_add_middleware(params: ProtocolParams) -> RuntimeCallFunctionOn
def _wrap_custom_command(method: str, params: ProtocolParams, session_id: str) -> RuntimeCallFunctionOnParams:
- return _call_function_params(
- "async function() { return JSON.stringify(await globalThis.ModCDP.handleCommand("
- f"{json.dumps(method)}, {json.dumps(params)}, {json.dumps(session_id)})); }}"
+ runtime_params = _call_function_params(
+ "async function(method, paramsJson, cdpSessionId) { "
+ "return JSON.stringify(await globalThis.ModCDP.handleCommand(method, JSON.parse(paramsJson), cdpSessionId)); "
+ "}"
)
+ runtime_params["arguments"] = [{"value": method}, {"value": json.dumps(params)}, {"value": session_id}]
+ return runtime_params
def _wrap_service_worker_command(
diff --git a/python/modcdp/transport/ReverseWebSocketUpstreamTransport.py b/python/modcdp/transport/ReverseWebSocketUpstreamTransport.py
index 0dffdd6..0fb940b 100644
--- a/python/modcdp/transport/ReverseWebSocketUpstreamTransport.py
+++ b/python/modcdp/transport/ReverseWebSocketUpstreamTransport.py
@@ -49,7 +49,7 @@ def update(self, config: dict[str, Any] | None = None) -> "ReverseWebSocketUpstr
return self
def getInjectorConfig(self) -> dict[str, Any]:
- return {"upstream_reversews_url": self.url}
+ return {}
def _setBind(self, bind: str) -> None:
parsed = urlparse(bind if "://" in bind else f"ws://{bind}")
diff --git a/python/pyproject.toml b/python/pyproject.toml
index b467f44..b2f0e3d 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "modcdp"
-version = "0.0.13"
+version = "0.0.19"
description = "Python client for ModCDP."
readme = "README.md"
requires-python = ">=3.11"
diff --git a/python/tests/test_AutoSessionRouter.py b/python/tests/test_AutoSessionRouter.py
index 177a2fd..0b9cd57 100644
--- a/python/tests/test_AutoSessionRouter.py
+++ b/python/tests/test_AutoSessionRouter.py
@@ -62,7 +62,7 @@ def test_bounds_detached_session_guards_and_clears_them_when_session_reattaches(
self.assertEqual(router.execution_contexts[recent_session_id], 43)
def test_tracks_real_target_sessions_and_execution_contexts(self) -> None:
- chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch()
+ chrome = LocalBrowserLauncher({"headless": True}).launch()
ws = create_connection(str(chrome["cdp_url"]), timeout=10)
lock = threading.Lock()
next_id = 0
@@ -86,7 +86,7 @@ def send(method: str, params: dict[str, Any] | None = None, session_id: str | No
result = response.get("result")
return result if isinstance(result, dict) else {}
- router = AutoSessionRouter(send, lambda: 5_000)
+ router = AutoSessionRouter(send, lambda: 30_000)
def reader() -> None:
while not closed:
@@ -121,11 +121,11 @@ def reader() -> None:
session_id = _wait_for(lambda: router.sessionIdForTarget(target_id))
context_result: Queue[int | BaseException] = Queue()
threading.Thread(
- target=lambda: _put_result(context_result, lambda: router.waitForExecutionContext(session_id, 5_000)),
+ target=lambda: _put_result(context_result, lambda: router.waitForExecutionContext(session_id, 30_000)),
daemon=True,
).start()
send("Runtime.enable", {}, session_id)
- context_id = context_result.get(timeout=10)
+ context_id = context_result.get(timeout=35)
if isinstance(context_id, BaseException):
raise context_id
self.assertIsInstance(context_id, int)
diff --git a/python/tests/test_BorrowedExtensionInjector.py b/python/tests/test_BorrowedExtensionInjector.py
index 90b03f8..38baa76 100644
--- a/python/tests/test_BorrowedExtensionInjector.py
+++ b/python/tests/test_BorrowedExtensionInjector.py
@@ -4,7 +4,6 @@
from pathlib import Path
from modcdp import ModCDPClient
-from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher
ROOT = Path(__file__).resolve().parents[2]
@@ -13,32 +12,39 @@
class BorrowedExtensionInjectorTests(unittest.TestCase):
def test_bootstraps_modcdp_inside_live_extension_service_worker(self) -> None:
- chrome = LocalBrowserLauncher(
- {
- "headless": True,
- "sandbox": False,
- "extra_args": [f"--load-extension={EXTENSION_PATH}"],
- }
- ).launch()
- cdp = ModCDPClient(
- launcher={"launcher_mode": "remote"},
- upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]},
+ owner = ModCDPClient(
+ launcher={"launcher_mode": "local", "launcher_options": {"headless": True}},
+ upstream={"upstream_mode": "ws"},
injector={
- "injector_mode": "borrow",
+ "injector_mode": "auto",
+ "injector_extension_path": str(EXTENSION_PATH),
"injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
"injector_trust_service_worker_target": True,
},
)
-
try:
- cdp.connect()
- self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "borrowed")
- self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf")
- target_infos = cdp.send("Target.getTargets")["targetInfos"]
- self.assertGreater(len(target_infos), 0)
+ owner.connect()
+ cdp = ModCDPClient(
+ launcher={"launcher_mode": "remote"},
+ upstream={"upstream_mode": "ws", "upstream_cdp_url": owner.cdp_url},
+ injector={
+ "injector_mode": "borrow",
+ "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
+ "injector_trust_service_worker_target": True,
+ },
+ )
+ try:
+ cdp.connect()
+ self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "borrowed")
+ self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf")
+ self.assertEqual(
+ cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"),
+ "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js",
+ )
+ finally:
+ cdp.close()
finally:
- cdp.close()
- chrome["close"]()
+ owner.close()
if __name__ == "__main__":
diff --git a/python/tests/test_DiscoveredExtensionInjector.py b/python/tests/test_DiscoveredExtensionInjector.py
index e62c709..46c71a7 100644
--- a/python/tests/test_DiscoveredExtensionInjector.py
+++ b/python/tests/test_DiscoveredExtensionInjector.py
@@ -1,169 +1,50 @@
from __future__ import annotations
-import json
-import shutil
-import tempfile
import unittest
from pathlib import Path
-from typing import Any, cast
from modcdp import ModCDPClient
-from modcdp.injector.DiscoveredExtensionInjector import DiscoveredExtensionInjector
-from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher
ROOT = Path(__file__).resolve().parents[2]
EXTENSION_PATH = ROOT / "dist" / "extension"
-CUSTOM_EXTENSION_ID = "hhklgmbgnbeghnjidampacgmgnhelifg"
-CUSTOM_EXTENSION_PUBLIC_KEY = (
- "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzG1LUbtH0aHMKjTAUeT0saY8xfnRNENctFJme3C1qnsqT7PAXMxJC4nT7tBZy2gEGRirBb3zIZ3OyAu9a0QR8lTLupDp4qHWOhQ7dl9ZjxjQdYa4Gby0xuXLdQrJIxDbmuv+UVJvYa8vRTwQB8koygbzDDDP5/YiB6mc0hbh8XBb82Ossy7T280k8280o/rS0CXdioUraCHj58PDhfxbs18TBcYfOjuRqua9J2oddxobtGehSD0gDtbvn2IWDtRajOlgZZyuS1vLoSR7C1ulFzpRSYPEMhI2x+wphut7E3QImyJ577YeULVGpt988FcixOou7udjx3/IUWjpq8046wIDAQAB"
-)
class DiscoveredExtensionInjectorTests(unittest.TestCase):
- def test_preserves_explicit_zero_probe_timeout(self) -> None:
- class ProbeTimeoutInjector(DiscoveredExtensionInjector):
- def __init__(self) -> None:
- super().__init__(
- {
- "injector_trust_service_worker_target": True,
- "injector_service_worker_probe_timeout_ms": 0,
- "injector_require_service_worker_target": False,
- }
- )
- self.waits: list[tuple[int, bool]] = []
-
- def _discoverReadyServiceWorker(self, *, matched_only: bool = False): # type: ignore[override]
- return None
-
- def _wakeConfiguredExtension(self): # type: ignore[override]
- return None
-
- def _waitForReadyServiceWorker(self, timeout_ms: int, matched_only: bool = False): # type: ignore[override]
- self.waits.append((timeout_ms, matched_only))
- return None
-
- injector = ProbeTimeoutInjector()
- self.assertIsNone(injector.inject())
- self.assertEqual(injector.waits, [(0, True)])
-
- def test_preserves_explicit_zero_ready_timeout(self) -> None:
- class ReadyTimeoutInjector(DiscoveredExtensionInjector):
- def __init__(self) -> None:
- super().__init__(
- {
- "injector_service_worker_ready_timeout_ms": 0,
- "injector_require_service_worker_target": True,
- }
- )
- self.waits: list[tuple[int, bool]] = []
-
- def _discoverReadyServiceWorker(self, *, matched_only: bool = False): # type: ignore[override]
- return None
-
- def _wakeConfiguredExtension(self): # type: ignore[override]
- return None
-
- def _waitForReadyServiceWorker(self, timeout_ms: int, matched_only: bool = False): # type: ignore[override]
- self.waits.append((timeout_ms, matched_only))
- return None
-
- injector = ReadyTimeoutInjector()
- with self.assertRaisesRegex(RuntimeError, "Required ModCDP service worker target was not visible"):
- injector.inject()
- self.assertEqual(injector.waits, [(0, False)])
-
def test_attaches_to_already_loaded_real_modcdp_extension(self) -> None:
- chrome = LocalBrowserLauncher(
- {
- "headless": True,
- "sandbox": False,
- "extra_args": [f"--load-extension={EXTENSION_PATH}"],
- }
- ).launch()
- cdp = ModCDPClient(
- launcher={"launcher_mode": "remote"},
- upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]},
+ owner = ModCDPClient(
+ launcher={"launcher_mode": "local", "launcher_options": {"headless": True}},
+ upstream={"upstream_mode": "ws"},
injector={
- "injector_mode": "discover",
+ "injector_mode": "auto",
+ "injector_extension_path": str(EXTENSION_PATH),
"injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
"injector_trust_service_worker_target": True,
},
)
-
try:
- cdp.connect()
- self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "discovered")
- self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf")
- self.assertEqual(
- cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"),
- "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js",
- )
- finally:
- cdp.close()
- chrome["close"]()
-
- def test_selects_configured_extension_when_multiple_modcdp_workers_exist(self) -> None:
- with tempfile.TemporaryDirectory(prefix="modcdp-custom-extension-") as custom_extension_dir:
- custom_extension_path = Path(custom_extension_dir)
- shutil.copytree(EXTENSION_PATH, custom_extension_path, dirs_exist_ok=True)
- manifest_path = custom_extension_path / "manifest.json"
- manifest = json.loads(manifest_path.read_text())
- manifest["key"] = CUSTOM_EXTENSION_PUBLIC_KEY
- manifest["name"] = "ModCDP Bridge Custom Test"
- manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
-
- chrome = LocalBrowserLauncher(
- {
- "headless": True,
- "sandbox": False,
- "extra_args": [f"--load-extension={EXTENSION_PATH},{custom_extension_path}"],
- }
- ).launch()
+ owner.connect()
cdp = ModCDPClient(
launcher={"launcher_mode": "remote"},
- upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]},
+ upstream={"upstream_mode": "ws", "upstream_cdp_url": owner.cdp_url},
injector={
"injector_mode": "discover",
- "injector_extension_id": CUSTOM_EXTENSION_ID,
"injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
"injector_trust_service_worker_target": True,
- "injector_require_service_worker_target": True,
},
)
-
try:
cdp.connect()
self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "discovered")
- self.assertEqual(cdp.extension_id, CUSTOM_EXTENSION_ID)
- self.assertEqual(cdp.Mod.evaluate(expression="chrome.runtime.id"), CUSTOM_EXTENSION_ID)
- targets = cast(dict[str, Any], cdp.sendRaw("Target.getTargets"))
- target_infos = targets.get("targetInfos")
- self.assertIsInstance(target_infos, list)
- target_infos = cast(list[Any], target_infos)
- modcdp_workers = [
- target
- for target in target_infos
- if isinstance(target, dict)
- and target.get("type") == "service_worker"
- and str(target.get("url", "")).endswith("/modcdp/service_worker.js")
- ]
- self.assertTrue(
- any(
- target.get("url") == f"chrome-extension://{CUSTOM_EXTENSION_ID}/modcdp/service_worker.js"
- for target in modcdp_workers
- )
- )
- self.assertTrue(
- any(
- target.get("url")
- == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js"
- for target in modcdp_workers
- )
+ self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf")
+ self.assertEqual(
+ cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"),
+ "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js",
)
finally:
cdp.close()
- chrome["close"]()
+ finally:
+ owner.close()
if __name__ == "__main__":
diff --git a/python/tests/test_ExtensionInjector.py b/python/tests/test_ExtensionInjector.py
index 7d50e9a..f1ea3ec 100644
--- a/python/tests/test_ExtensionInjector.py
+++ b/python/tests/test_ExtensionInjector.py
@@ -1,202 +1,12 @@
from __future__ import annotations
-import json
-import time
import unittest
-from pathlib import Path
-from typing import Any, cast
-import modcdp
-from modcdp.injector.ExtensionInjector import ExtensionInjector, ExtensionInjectorConfig
-from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher
-from modcdp.types import ProtocolParams, ProtocolResult
-from websocket import create_connection
-
-
-ROOT = Path(__file__).resolve().parents[2]
-EXTENSION_PATH = ROOT / "dist" / "extension"
-
-
-class ProbeExtensionInjector(ExtensionInjector):
- def inject(self):
- return self._waitForReadyServiceWorker(
- self.options.get("injector_service_worker_ready_timeout_ms") or 60_000,
- matched_only=True,
- )
-
- def sendTimed(
- self,
- method: str,
- params: ProtocolParams | None,
- session_id: str | None,
- timeout_ms: int,
- ) -> ProtocolResult:
- return self._sendWithTimeout(method, params or {}, session_id, timeout_ms)
+from modcdp.injector.ExtensionInjector import ExtensionInjector
class ExtensionInjectorTests(unittest.TestCase):
- def test_probes_real_extension_service_worker_with_shared_base_config(self) -> None:
- chrome = LocalBrowserLauncher(
- {
- "headless": True,
- "sandbox": False,
- "extra_args": [f"--load-extension={EXTENSION_PATH}"],
- }
- ).launch()
- ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10)
- next_id = 0
-
- def send(method: str, params: ProtocolParams | None = None, session_id: str | None = None) -> ProtocolResult:
- nonlocal next_id
- next_id += 1
- message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}}
- if session_id:
- message["sessionId"] = session_id
- ws.send(json.dumps(message))
- while True:
- response = json.loads(ws.recv())
- if response.get("id") != next_id:
- continue
- error = response.get("error")
- if isinstance(error, dict):
- raise RuntimeError(str(error.get("message") or error))
- return cast(ProtocolResult, response.get("result") or {})
-
- def attach_to_target(target_id: str) -> str | None:
- result = send("Target.attachToTarget", {"targetId": target_id, "flatten": True})
- session_id = result.get("sessionId")
- return session_id if isinstance(session_id, str) else None
-
- injector = ProbeExtensionInjector(
- cast(ExtensionInjectorConfig, {
- "send": send,
- "attachToTarget": attach_to_target,
- "injector_extension_id": "mdedooklbnfejodmnhmkdpkaedafkehf",
- "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
- "injector_trust_service_worker_target": True,
- })
- )
-
- try:
- self.assertEqual(injector.getLauncherConfig(), {})
- self.assertEqual(injector.getTransportConfig(), {"injector_extension_id": "mdedooklbnfejodmnhmkdpkaedafkehf"})
- result = injector.inject()
- self.assertEqual(result["extension_id"] if result else None, "mdedooklbnfejodmnhmkdpkaedafkehf")
- self.assertTrue(str(result["url"] if result else "").endswith("/modcdp/service_worker.js"))
- finally:
- injector.close()
- ws.close()
- chrome["close"]()
-
- def test_keeps_modcdp_service_worker_alive_through_offscreen_keepalive(self) -> None:
- chrome = LocalBrowserLauncher(
- {
- "headless": True,
- "sandbox": False,
- "extra_args": [f"--load-extension={EXTENSION_PATH}"],
- }
- ).launch()
- ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10)
- next_id = 0
-
- def send(method: str, params: ProtocolParams | None = None, session_id: str | None = None) -> ProtocolResult:
- nonlocal next_id
- next_id += 1
- message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}}
- if session_id:
- message["sessionId"] = session_id
- ws.send(json.dumps(message))
- while True:
- response = json.loads(ws.recv())
- if response.get("id") != next_id:
- continue
- error = response.get("error")
- if isinstance(error, dict):
- raise RuntimeError(str(error.get("message") or error))
- return cast(ProtocolResult, response.get("result") or {})
-
- def attach_to_target(target_id: str) -> str | None:
- result = send("Target.attachToTarget", {"targetId": target_id, "flatten": True})
- session_id = result.get("sessionId")
- return session_id if isinstance(session_id, str) else None
-
- injector = ProbeExtensionInjector(
- cast(ExtensionInjectorConfig, {
- "send": send,
- "attachToTarget": attach_to_target,
- "injector_extension_id": "mdedooklbnfejodmnhmkdpkaedafkehf",
- "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
- "injector_trust_service_worker_target": True,
- })
- )
-
- try:
- result = injector.inject()
- self.assertEqual(result["extension_id"] if result else None, "mdedooklbnfejodmnhmkdpkaedafkehf")
- session_id = result["session_id"] if result else None
- self.assertIsInstance(session_id, str)
-
- value: list[Any] = []
- for _ in range(50):
- contexts = send(
- "Runtime.evaluate",
- {
- "expression": (
- "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => "
- "({ type: context.contextType, url: context.documentUrl || context.origin || '' })))"
- ),
- "awaitPromise": True,
- "returnByValue": True,
- },
- cast(str, session_id),
- )
- raw_value = cast(dict[str, Any], contexts.get("result") or {}).get("value")
- value = raw_value if isinstance(raw_value, list) else []
- if any(
- isinstance(context, dict)
- and context.get("type") == "OFFSCREEN_DOCUMENT"
- and context.get("url")
- == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html"
- for context in value
- ):
- break
- time.sleep(0.1)
- self.assertTrue(
- any(
- isinstance(context, dict)
- and context.get("type") == "OFFSCREEN_DOCUMENT"
- and context.get("url")
- == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html"
- for context in value
- )
- )
-
- time.sleep(3)
- targets = cast(dict[str, Any], send("Target.getTargets"))
- target_infos = targets.get("targetInfos")
- self.assertIsInstance(target_infos, list)
- target_infos = cast(list[Any], target_infos)
- self.assertTrue(
- any(
- isinstance(target, dict)
- and target.get("type") == "service_worker"
- and target.get("url")
- == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js"
- for target in target_infos
- )
- )
- version = send(
- "Runtime.evaluate",
- {"expression": "globalThis.ModCDP?.__ModCDPServerVersion", "returnByValue": True},
- cast(str, session_id),
- )
- self.assertEqual(cast(dict[str, Any], version.get("result") or {}).get("value"), 2)
- finally:
- injector.close()
- ws.close()
- chrome["close"]()
-
- def test_owns_shared_injector_config_and_runtime_transport_config(self) -> None:
+ def test_owns_shared_injector_config(self) -> None:
injector = ExtensionInjector(
{
"injector_extension_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
@@ -215,114 +25,19 @@ def test_owns_shared_injector_config_and_runtime_transport_config(self) -> None:
}
)
)
-
- with self.assertRaisesRegex(NotImplementedError, "ExtensionInjector.inject is not implemented"):
- injector.inject()
-
- def test_send_with_timeout_enforces_cdp_send_timeout(self) -> None:
- chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch()
- ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10)
- next_id = 0
-
- def send(method: str, params: ProtocolParams | None = None, session_id: str | None = None) -> ProtocolResult:
- nonlocal next_id
- next_id += 1
- message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}}
- if session_id:
- message["sessionId"] = session_id
- ws.send(json.dumps(message))
- while True:
- response = json.loads(ws.recv())
- if response.get("id") != next_id:
- continue
- error = response.get("error")
- if isinstance(error, dict):
- raise RuntimeError(str(error.get("message") or error))
- return cast(ProtocolResult, response.get("result") or {})
-
- injector = ProbeExtensionInjector(cast(ExtensionInjectorConfig, {"send": send}))
- target_id: str | None = None
-
- try:
- created = send("Target.createTarget", {"url": "about:blank#modcdp-timeout"})
- target_id = cast(str, created["targetId"])
- attached = send("Target.attachToTarget", {"targetId": target_id, "flatten": True})
- session_id = cast(str, attached["sessionId"])
- send("Runtime.enable", {}, session_id)
- with self.assertRaisesRegex(TimeoutError, r"Runtime\.evaluate timed out after 5ms"):
- injector.sendTimed(
- "Runtime.evaluate",
- {"expression": "new Promise(() => {})", "awaitPromise": True},
- session_id,
- 5,
- )
- finally:
- if target_id:
- try:
- send("Target.closeTarget", {"targetId": target_id})
- except Exception:
- pass
- injector.close()
- ws.close()
- chrome["close"]()
-
- def test_wakes_configured_extension_with_hidden_background_target(self) -> None:
- chrome = LocalBrowserLauncher(
- {
- "headless": True,
- "sandbox": False,
- "extra_args": [f"--load-extension={EXTENSION_PATH}"],
- }
- ).launch()
- ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10)
- next_id = 0
-
- def send(method: str, params: ProtocolParams | None = None, _session_id: str | None = None) -> ProtocolResult:
- nonlocal next_id
- next_id += 1
- message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}}
- ws.send(json.dumps(message))
- while True:
- response = json.loads(ws.recv())
- if response.get("id") != next_id:
- continue
- error = response.get("error")
- if isinstance(error, dict):
- raise RuntimeError(str(error.get("message") or error))
- return cast(ProtocolResult, response.get("result") or {})
-
- injector = ProbeExtensionInjector(
- cast(ExtensionInjectorConfig, {
- "injector_extension_id": "mdedooklbnfejodmnhmkdpkaedafkehf",
- "send": send,
- })
- )
-
- try:
- self.assertTrue(injector._wakeConfiguredExtension())
- targets = cast(dict[str, Any], send("Target.getTargets"))
- target_infos = targets.get("targetInfos")
- self.assertIsInstance(target_infos, list)
- target_infos = cast(list[Any], target_infos)
- self.assertTrue(
- any(
- isinstance(target, dict)
- and target.get("url") == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/wake.html"
- for target in target_infos
- )
+ self.assertFalse(
+ injector._serviceWorkerTargetMatches(
+ {
+ "targetId": "target-1",
+ "type": "service_worker",
+ "url": "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/background.js",
+ }
)
- finally:
- injector.close()
- ws.close()
- chrome["close"]()
+ )
- def test_package_exports_all_injector_classes(self) -> None:
- self.assertIs(modcdp.ExtensionInjector, ExtensionInjector)
- self.assertEqual(modcdp.DiscoveredExtensionInjector.__name__, "DiscoveredExtensionInjector")
- self.assertEqual(modcdp.LocalBrowserLaunchExtensionInjector.__name__, "LocalBrowserLaunchExtensionInjector")
- self.assertEqual(modcdp.BBBrowserExtensionInjector.__name__, "BBBrowserExtensionInjector")
- self.assertEqual(modcdp.ExtensionsLoadUnpackedInjector.__name__, "ExtensionsLoadUnpackedInjector")
- self.assertEqual(modcdp.BorrowedExtensionInjector.__name__, "BorrowedExtensionInjector")
+ def test_base_inject_reports_the_class_name(self) -> None:
+ with self.assertRaisesRegex(NotImplementedError, "ExtensionInjector.inject is not implemented"):
+ ExtensionInjector().inject()
if __name__ == "__main__":
diff --git a/python/tests/test_ExtensionsLoadUnpackedInjector.py b/python/tests/test_ExtensionsLoadUnpackedInjector.py
index 5fd9d55..4a101d8 100644
--- a/python/tests/test_ExtensionsLoadUnpackedInjector.py
+++ b/python/tests/test_ExtensionsLoadUnpackedInjector.py
@@ -1,73 +1,25 @@
from __future__ import annotations
-import json
-import re
import unittest
from pathlib import Path
-from typing import Any, cast
+from typing import cast
from modcdp.injector.ExtensionsLoadUnpackedInjector import ExtensionsLoadUnpackedInjector
-from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher
-from websocket import create_connection
-
-
-ROOT = Path(__file__).resolve().parents[2]
-EXTENSION_PATH = ROOT / "dist" / "extension"
class ExtensionsLoadUnpackedInjectorTests(unittest.TestCase):
- def test_prepares_default_packaged_extension_zip_when_path_is_omitted(self) -> None:
+ def test_prepares_default_packaged_extension_zip(self) -> None:
injector = ExtensionsLoadUnpackedInjector()
try:
injector.prepare()
unpacked_extension_path = injector.unpacked_extension_path
self.assertIsInstance(unpacked_extension_path, str)
unpacked_extension_path = cast(str, unpacked_extension_path)
+ self.assertIn("modcdp-extension-", unpacked_extension_path)
self.assertTrue((Path(unpacked_extension_path) / "manifest.json").exists())
- self.assertTrue(str(injector.options.get("injector_extension_path", "")).endswith("extension.zip"))
finally:
injector.close()
- def test_exercises_real_cdp_load_unpacked_path(self) -> None:
- chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch()
- ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10)
- next_id = 0
-
- def send(method: str, params: dict[str, Any] | None = None, session_id: str | None = None) -> dict[str, Any]:
- nonlocal next_id
- next_id += 1
- message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}}
- if session_id:
- message["sessionId"] = session_id
- ws.send(json.dumps(message))
- while True:
- response = json.loads(ws.recv())
- if response.get("id") != next_id:
- continue
- error = response.get("error")
- if isinstance(error, dict):
- raise RuntimeError(str(error.get("message") or error))
- return cast(dict[str, Any], response.get("result") or {})
-
- injector = ExtensionsLoadUnpackedInjector(
- cast(Any, {
- "send": send,
- "injector_extension_path": str(EXTENSION_PATH),
- })
- )
- try:
- injector.prepare()
- result = injector.inject()
- self.assertIsNone(result)
- self.assertIsNotNone(injector.last_error)
- self.assertRegex(
- str(injector.last_error),
- re.compile(r"Method not available|Method.*not.*found|wasn't found", re.I),
- )
- finally:
- injector.close()
- ws.close()
- chrome["close"]()
if __name__ == "__main__":
unittest.main()
diff --git a/python/tests/test_LocalBrowserLaunchExtensionInjector.py b/python/tests/test_LocalBrowserLaunchExtensionInjector.py
index 4f33892..3252463 100644
--- a/python/tests/test_LocalBrowserLaunchExtensionInjector.py
+++ b/python/tests/test_LocalBrowserLaunchExtensionInjector.py
@@ -1,12 +1,14 @@
from __future__ import annotations
import unittest
+import time
+import tempfile
+import zipfile
from pathlib import Path
-from typing import cast
+from typing import Any, cast
from modcdp.injector.ExtensionInjector import DEFAULT_MODCDP_EXTENSION_ID
from modcdp.injector.LocalBrowserLaunchExtensionInjector import LocalBrowserLaunchExtensionInjector
-from modcdp.client.ModCDPClient import ModCDPClient
ROOT = Path(__file__).resolve().parents[2]
@@ -14,46 +16,35 @@
class LocalBrowserLaunchExtensionInjectorTests(unittest.TestCase):
- def test_loads_real_extension_during_local_launch(self) -> None:
- cdp = ModCDPClient(
- launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}},
- upstream={"upstream_mode": "ws"},
- injector={
- "injector_mode": "inject",
- "injector_extension_path": str(EXTENSION_PATH),
- "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
- "injector_trust_service_worker_target": True,
- "injector_service_worker_probe_timeout_ms": 30_000,
- },
- client={
- "client_cdp_send_timeout_ms": 30_000,
- },
- )
+ def test_rejects_zip_entries_outside_extraction_directory(self) -> None:
+ with tempfile.TemporaryDirectory(prefix="modcdp-bad-zip-") as temp_dir:
+ zip_path = Path(temp_dir) / "extension.zip"
+ with zipfile.ZipFile(zip_path, "w") as archive:
+ archive.writestr("../evil.txt", "evil")
- try:
- cdp.connect()
- self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "local_launch")
- self.assertEqual(cdp.extension_id, DEFAULT_MODCDP_EXTENSION_ID)
- self.assertRegex(cdp.ext_session_id or "", r"^.+$")
- self.assertEqual(
- cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"),
- f"chrome-extension://{DEFAULT_MODCDP_EXTENSION_ID}/modcdp/service_worker.js",
- )
- finally:
- cdp.close()
+ injector = LocalBrowserLaunchExtensionInjector({"injector_extension_path": str(zip_path)})
+ try:
+ with self.assertRaisesRegex(RuntimeError, "escapes extension extraction directory"):
+ injector.prepare()
+ self.assertFalse((Path(temp_dir) / "evil.txt").exists())
+ finally:
+ injector.close()
- def test_prepares_launcher_config(self) -> None:
+ def test_prepares_unpacked_extension_directory_for_load_extension(self) -> None:
injector = LocalBrowserLaunchExtensionInjector({"injector_extension_path": str(EXTENSION_PATH)})
try:
injector.prepare()
- extra_args = injector.getLauncherConfig().get("extra_args") or []
- self.assertEqual(len(extra_args), 1)
- self.assertTrue(extra_args[0].startswith("--load-extension="))
+ unpacked_extension_path = injector.unpacked_extension_path
+ self.assertIsInstance(unpacked_extension_path, str)
+ unpacked_extension_path = cast(str, unpacked_extension_path)
+ self.assertNotEqual(unpacked_extension_path, str(EXTENSION_PATH))
+ self.assertTrue((Path(unpacked_extension_path) / "manifest.json").exists())
+ self.assertEqual(injector.getLauncherConfig(), {"extra_args": [f"--load-extension={unpacked_extension_path}"]})
self.assertEqual(injector.options.get("injector_extension_id"), DEFAULT_MODCDP_EXTENSION_ID)
finally:
injector.close()
- def test_prepares_default_packaged_extension_zip_when_path_is_omitted(self) -> None:
+ def test_prepares_default_extension_zip_for_load_extension(self) -> None:
injector = LocalBrowserLaunchExtensionInjector()
try:
injector.prepare()
@@ -61,13 +52,39 @@ def test_prepares_default_packaged_extension_zip_when_path_is_omitted(self) -> N
self.assertIsInstance(unpacked_extension_path, str)
unpacked_extension_path = cast(str, unpacked_extension_path)
self.assertTrue((Path(unpacked_extension_path) / "manifest.json").exists())
- self.assertTrue(str(injector.options.get("injector_extension_path", "")).endswith("extension.zip"))
- extra_args = injector.getLauncherConfig().get("extra_args") or []
- self.assertEqual(extra_args, [f"--load-extension={unpacked_extension_path}"])
+ self.assertIn("modcdp-extension-", unpacked_extension_path)
+ self.assertEqual(injector.getLauncherConfig(), {"extra_args": [f"--load-extension={unpacked_extension_path}"]})
self.assertEqual(injector.options.get("injector_extension_id"), DEFAULT_MODCDP_EXTENSION_ID)
finally:
injector.close()
+ def test_returns_immediately_when_launched_extension_target_is_absent(self) -> None:
+ methods: list[str] = []
+
+ def send(method: str, params: dict[str, Any] | None = None, session_id: str | None = None) -> dict[str, Any]:
+ methods.append(method)
+ if method == "Target.getTargets":
+ return {"targetInfos": []}
+ raise RuntimeError(f"unexpected {method}")
+
+ injector = LocalBrowserLaunchExtensionInjector(
+ cast(Any, {
+ "injector_extension_path": str(EXTENSION_PATH),
+ "injector_trust_service_worker_target": True,
+ "send": send,
+ })
+ )
+ try:
+ injector.prepare()
+ started_at = time.perf_counter()
+ result = injector.inject()
+ elapsed_ms = (time.perf_counter() - started_at) * 1000
+ self.assertIsNone(result)
+ self.assertEqual(methods, ["Target.getTargets"])
+ self.assertLess(elapsed_ms, 200)
+ finally:
+ injector.close()
+
if __name__ == "__main__":
unittest.main()
diff --git a/python/tests/test_LocalBrowserLauncher.py b/python/tests/test_LocalBrowserLauncher.py
index 678c9ee..2816a22 100644
--- a/python/tests/test_LocalBrowserLauncher.py
+++ b/python/tests/test_LocalBrowserLauncher.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
+import sys
import tempfile
import unittest
from pathlib import Path
@@ -15,12 +16,11 @@ def test_class_helpers_match_ts_surface(self) -> None:
self.assertIsInstance(LocalBrowserLauncher.findChromeBinary(), str)
self.assertIsInstance(LocalBrowserLauncher.freePort(), int)
- def test_launches_real_browser_and_speaks_cdp(self) -> None:
+ def test_launches_real_browser_over_chosen_cdp_port_and_honors_launch_options(self) -> None:
with tempfile.TemporaryDirectory(prefix="modcdp-python-local-profile-") as user_data_dir:
chrome = LocalBrowserLauncher(
{
"headless": True,
- "sandbox": False,
"chrome_ready_timeout_ms": 45_000,
"chrome_ready_poll_interval_ms": 50,
}
@@ -37,6 +37,16 @@ def test_launches_real_browser_and_speaks_cdp(self) -> None:
self.assertEqual(version["id"], 1)
self.assertIn("Chrome", version["result"]["product"])
self.assertIsInstance(version["result"]["protocolVersion"], str)
+ ws.send(json.dumps({"id": 2, "method": "SystemInfo.getInfo", "params": {}}))
+ system_info = json.loads(ws.recv())
+ self.assertEqual(system_info["id"], 2)
+ command_line = system_info["result"]["commandLine"]
+ self.assertIsInstance(command_line, str)
+ self.assertIn("--window-size=900,700", command_line)
+ if sys.platform.startswith("linux"):
+ self.assertIn("--no-sandbox", command_line)
+ else:
+ self.assertNotIn("--no-sandbox", command_line)
finally:
ws.close()
chrome["close"]()
@@ -48,7 +58,6 @@ def test_cleanup_user_data_dir_removes_explicit_profile(self) -> None:
chrome = LocalBrowserLauncher(
{
"headless": True,
- "sandbox": False,
"chrome_ready_timeout_ms": 45_000,
}
).launch({"user_data_dir": user_data_dir, "cleanup_user_data_dir": True})
@@ -63,7 +72,6 @@ def test_launches_real_browser_over_remote_debugging_pipe(self) -> None:
chrome = LocalBrowserLauncher(
{
"headless": True,
- "sandbox": False,
"remote_debugging": "pipe",
"chrome_ready_timeout_ms": 45_000,
}
@@ -88,7 +96,6 @@ def test_launches_pipe_browser_with_auxiliary_loopback_only_when_requested(self)
chrome = LocalBrowserLauncher(
{
"headless": True,
- "sandbox": False,
"remote_debugging": "pipe",
"loopback_cdp": True,
"chrome_ready_timeout_ms": 45_000,
diff --git a/python/tests/test_ModCDPClient.py b/python/tests/test_ModCDPClient.py
index fcbc54f..cd2060c 100644
--- a/python/tests/test_ModCDPClient.py
+++ b/python/tests/test_ModCDPClient.py
@@ -6,8 +6,6 @@
import time
import unittest
from collections.abc import Mapping
-from contextlib import redirect_stderr
-from io import StringIO
from pathlib import Path
from queue import Empty, Queue
from typing import Any, cast
@@ -17,6 +15,7 @@
from modcdp import ModCDPClient
from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher
from modcdp.types import JsonValue
+from tests.test_ReverseWebSocketUpstreamTransport import reversews_test_browser_path
HERE = Path(__file__).resolve().parent
@@ -182,6 +181,22 @@ def test_defaults_launched_modcdp_server_upstreams_to_extension_auto(self) -> No
self.assertEqual(attach_only.upstream_endpoint_kind, "modcdp_server")
self.assertEqual(attach_only.injector["injector_mode"], "none")
+ def test_orders_local_auto_injection_as_launch_flag_then_load_unpacked_fallback(self) -> None:
+ cdp = ModCDPClient(
+ launcher={"launcher_mode": "local"},
+ injector={"injector_mode": "auto"},
+ )
+
+ self.assertEqual(
+ [type(injector).__name__ for injector in cdp._extension_injectors_for_config()],
+ [
+ "LocalBrowserLaunchExtensionInjector",
+ "ExtensionsLoadUnpackedInjector",
+ "DiscoveredExtensionInjector",
+ "BorrowedExtensionInjector",
+ ],
+ )
+
def test_rejects_unknown_component_modes_at_their_owning_factory_boundary(self) -> None:
with self.assertRaisesRegex(RuntimeError, r"unknown upstream\.upstream_mode=bogus"):
ModCDPClient(upstream={"upstream_mode": "bogus"})._upstream_transport()
@@ -190,12 +205,13 @@ def test_rejects_unknown_component_modes_at_their_owning_factory_boundary(self)
with self.assertRaisesRegex(RuntimeError, r"unknown injector\.injector_mode=bogus"):
ModCDPClient(injector={"injector_mode": "bogus"})._extension_injectors_for_config()
- def test_connects_with_local_launch_and_injector_chain(self) -> None:
+ def test_connects_with_local_launch_injector_chain(self) -> None:
cdp = ModCDPClient(
- launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}},
+ launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "chrome_ready_timeout_ms": 60_000}},
upstream={"upstream_mode": "ws"},
injector={
- "injector_mode": "inject",
+ "injector_mode": "auto",
+ "injector_extension_path": str(EXTENSION_PATH),
"injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
"injector_trust_service_worker_target": True,
"injector_service_worker_probe_timeout_ms": 30_000,
@@ -208,12 +224,29 @@ def test_connects_with_local_launch_and_injector_chain(self) -> None:
try:
cdp.connect()
- self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "local_launch")
+ self.assertIn(
+ cdp.connect_timing.get("injector_source") if cdp.connect_timing else None,
+ ("discovered", "local_launch", "extensions_load_unpacked", "borrowed"),
+ )
self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf")
self.assertEqual(
cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"),
"chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js",
)
+ contexts = cdp.Mod.evaluate(
+ expression=(
+ "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => "
+ "({ type: context.contextType, url: context.documentUrl || context.origin || '' })))"
+ )
+ )
+ self.assertTrue(
+ any(
+ isinstance(context, Mapping)
+ and context.get("type") == "OFFSCREEN_DOCUMENT"
+ and context.get("url") == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html"
+ for context in cast(list[Any], contexts)
+ )
+ )
sent_at = int(time.time() * 1000)
pong: Queue[Mapping[str, Any]] = Queue()
@@ -251,7 +284,10 @@ def test_close_does_not_close_a_remote_browser_it_did_not_launch(self) -> None:
chrome = LocalBrowserLauncher(
{
"headless": True,
- "sandbox": False,
+ "chrome_ready_timeout_ms": 60_000,
+ # This test manually supplies --load-extension, so it intentionally uses
+ # the launch-flag browser path instead of relying on the client fallback.
+ "executable_path": reversews_test_browser_path(),
"extra_args": [f"--load-extension={EXTENSION_PATH}"],
}
).launch()
@@ -260,10 +296,14 @@ def test_close_does_not_close_a_remote_browser_it_did_not_launch(self) -> None:
launcher={"launcher_mode": "remote"},
upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]},
injector={
- "injector_mode": "discover",
+ "injector_mode": "auto",
+ "injector_extension_path": str(EXTENSION_PATH),
"injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
"injector_trust_service_worker_target": True,
+ "injector_service_worker_ready_timeout_ms": 30_000,
+ "injector_service_worker_probe_timeout_ms": 30_000,
},
+ client={"client_routes": {"*.*": "direct_cdp"}},
)
try:
@@ -281,11 +321,17 @@ def test_close_does_not_close_a_remote_browser_it_did_not_launch(self) -> None:
def test_close_keeps_injector_files_until_after_launched_browser_shutdown(self) -> None:
cdp = ModCDPClient(
- launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}},
- upstream={
- "upstream_mode": "reversews",
- "upstream_reversews_wait_timeout_ms": 30_000,
+ launcher={
+ "launcher_mode": "local",
+ "launcher_options": {
+ "headless": True,
+ # After explicit CHROME_PATH and CI /usr/bin/chromium, this test uses
+ # Chrome for Testing because Canary rejects --load-extension in this
+ # local launch injector path.
+ "executable_path": reversews_test_browser_path(),
+ },
},
+ upstream={"upstream_mode": "ws"},
injector={
"injector_mode": "auto",
"injector_extension_path": str(EXTENSION_PATH),
@@ -332,7 +378,7 @@ def close_browser() -> None:
def test_close_clears_top_level_connection_state(self) -> None:
cdp = ModCDPClient(
- launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}},
+ launcher={"launcher_mode": "local", "launcher_options": {"headless": True}},
upstream={"upstream_mode": "ws"},
injector={
"injector_mode": "auto",
@@ -351,7 +397,7 @@ def test_close_clears_top_level_connection_state(self) -> None:
def test_generated_cdp_surface_exposes_direct_domain_commands(self) -> None:
client = ModCDPClient(
- launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}},
+ launcher={"launcher_mode": "local", "launcher_options": {"headless": True}},
upstream={"upstream_mode": "ws"},
injector={
"injector_mode": "auto",
@@ -404,7 +450,7 @@ async def run_awaited_calls() -> None:
self.assertRegex(str(awaited_raw_result["targetId"]), r"^[A-F0-9]+$")
client = ModCDPClient(
- launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}},
+ launcher={"launcher_mode": "local", "launcher_options": {"headless": True}},
upstream={"upstream_mode": "ws"},
injector={
"injector_mode": "auto",
@@ -599,18 +645,17 @@ def test_custom_event_schema_validates_payload_before_handlers(self) -> None:
payload: dict[str, JsonValue] = {"url": "https://example.com", "ready": True}
self.assertEqual(client._validate_event_payload("Custom.ready", payload), payload)
- with redirect_stderr(StringIO()):
- self.assertIsNone(client._validate_event_payload("Custom.ready", {"url": "http://example.com", "ready": True}))
- self.assertIsNone(
- client._validate_event_payload("Custom.ready", {"url": "https://example.com", "ready": True, "x": 1})
- )
+ with self.assertRaises(ValueError):
+ client._validate_event_payload("Custom.ready", {"url": "http://example.com", "ready": True})
+ with self.assertRaises(ValueError):
+ client._validate_event_payload("Custom.ready", {"url": "https://example.com", "ready": True, "x": 1})
def test_scalar_event_schema_validates_value_payloads(self) -> None:
client = ModCDPClient(custom_events=[{"name": "Custom.count", "event_schema": {"type": "integer", "minimum": 1}}])
self.assertEqual(client._validate_event_payload("Custom.count", {"value": 3}), {"value": 3})
- with redirect_stderr(StringIO()):
- self.assertIsNone(client._validate_event_payload("Custom.count", {"value": 0}))
+ with self.assertRaises(ValueError):
+ client._validate_event_payload("Custom.count", {"value": 0})
if __name__ == "__main__":
diff --git a/python/tests/test_ModCDPClientCustomFlatNamespace.py b/python/tests/test_ModCDPClientCustomFlatNamespace.py
index 8bb8128..1872550 100644
--- a/python/tests/test_ModCDPClientCustomFlatNamespace.py
+++ b/python/tests/test_ModCDPClientCustomFlatNamespace.py
@@ -1,8 +1,6 @@
from __future__ import annotations
import asyncio
-from contextlib import redirect_stderr
-from io import StringIO
from queue import Queue
import unittest
from pathlib import Path
@@ -27,7 +25,7 @@ class ResultSchema(BaseModel):
client = ModCDPClient(
launcher={
"launcher_mode": "local",
- "launcher_options": {"headless": True, "sandbox": False},
+ "launcher_options": {"headless": True},
},
upstream={"upstream_mode": "ws"},
injector={
@@ -68,7 +66,7 @@ class EventSchema(BaseModel):
client = ModCDPClient(
launcher={
"launcher_mode": "local",
- "launcher_options": {"headless": True, "sandbox": False},
+ "launcher_options": {"headless": True},
},
upstream={"upstream_mode": "ws"},
injector={
@@ -117,8 +115,8 @@ def test_schema_only_custom_event_registers_without_websocket(self) -> None:
self.assertEqual(result, {"name": "Custom.schemaOnly", "registered": True})
self.assertEqual(client._validate_event_payload("Custom.schemaOnly", {"ok": True}), {"ok": True})
- with redirect_stderr(StringIO()):
- self.assertIsNone(client._validate_event_payload("Custom.schemaOnly", {"ok": True, "extra": True}))
+ with self.assertRaises(ValueError):
+ client._validate_event_payload("Custom.schemaOnly", {"ok": True, "extra": True})
if __name__ == "__main__":
diff --git a/python/tests/test_ModCDPClientRoutedDefaultOverrides.py b/python/tests/test_ModCDPClientRoutedDefaultOverrides.py
index 68182b1..24db6c3 100644
--- a/python/tests/test_ModCDPClientRoutedDefaultOverrides.py
+++ b/python/tests/test_ModCDPClientRoutedDefaultOverrides.py
@@ -5,7 +5,7 @@
from typing import Any, cast
import unittest
-from modcdp import LocalBrowserLauncher, ModCDPClient
+from modcdp import ModCDPClient
HERE = Path(__file__).resolve().parent
@@ -71,22 +71,28 @@ def target_infos_from_result(result: Any) -> list[dict[str, Any]]:
class ModCDPClientRoutedDefaultOverridesTests(unittest.TestCase):
def test_service_worker_routed_standard_cdp_commands_and_events_can_be_transformed(self) -> None:
- chrome = LocalBrowserLauncher(
- {
- "headless": True,
- "sandbox": False,
- "extra_args": [f"--load-extension={EXTENSION_PATH}"],
- }
- ).launch()
- cdp = ModCDPClient(
- launcher={"launcher_mode": "remote"},
- upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]},
+ owner = ModCDPClient(
+ launcher={
+ "launcher_mode": "local",
+ "launcher_options": {"headless": True},
+ },
+ upstream={"upstream_mode": "ws"},
injector={
"injector_mode": "auto",
"injector_extension_path": str(EXTENSION_PATH),
"injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
"injector_trust_service_worker_target": True,
},
+ )
+ owner.connect()
+ cdp = ModCDPClient(
+ launcher={"launcher_mode": "remote"},
+ upstream={"upstream_mode": "ws", "upstream_cdp_url": owner.cdp_url},
+ injector={
+ "injector_mode": "discover",
+ "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
+ "injector_trust_service_worker_target": True,
+ },
client={
"client_routes": {
"Target.getTargets": "service_worker",
@@ -94,17 +100,17 @@ def test_service_worker_routed_standard_cdp_commands_and_events_can_be_transform
"Target.setDiscoverTargets": "service_worker",
}
},
- server={"server_loopback_cdp_url": chrome["cdp_url"], "server_routes": {"*.*": "loopback_cdp"}},
+ server={"server_loopback_cdp_url": owner.cdp_url, "server_routes": {"*.*": "loopback_cdp"}},
)
try:
cdp.connect()
- self.assertEqual(cdp.cdp_url, chrome["cdp_url"])
+ self.assertEqual(cdp.cdp_url, owner.cdp_url)
self.assertIsNotNone(cdp.server)
server = cast(dict[str, Any], cdp.server)
- self.assertEqual(server["server_loopback_cdp_url"], chrome["cdp_url"])
+ self.assertEqual(server["server_loopback_cdp_url"], owner.cdp_url)
- raw_targets = cdp._send_message("Target.getTargets", {})
+ raw_targets = cdp.send("Target.getTargets")
raw_target_infos = target_infos_from_result(raw_targets)
self.assertTrue(raw_target_infos)
self.assertFalse(any("tabId" in target_info for target_info in raw_target_infos))
@@ -160,7 +166,7 @@ def on_target_created(params):
except Exception:
pass
cdp.close()
- chrome["close"]()
+ owner.close()
if __name__ == "__main__":
diff --git a/python/tests/test_ModCDPClient_protocol_validation.py b/python/tests/test_ModCDPClient_protocol_validation.py
new file mode 100644
index 0000000..b672ea4
--- /dev/null
+++ b/python/tests/test_ModCDPClient_protocol_validation.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import unittest
+from typing import TYPE_CHECKING, assert_type
+
+from pydantic import BaseModel
+
+from modcdp import ModCDPClient
+from modcdp.client.ModCDPClient import AwaitableValue
+from modcdp.types.generated.cdp import RuntimeDomain, TargetDomain
+from modcdp.types.generated.cdp import AwaitableDict
+from modcdp.types.modcdp import JsonValue, ProtocolPayload
+
+
+class CustomEchoParams(BaseModel):
+ text: str
+
+
+class CustomEchoResult(BaseModel):
+ ok: bool
+
+
+class CustomReadyEvent(BaseModel):
+ ok: bool
+
+
+class ModCDPClientProtocolValidationTests(unittest.TestCase):
+ def test_protocol_validation_covers_native_methods_native_events_custom_methods_custom_events_and_native_overrides(self) -> None:
+ client = ModCDPClient()
+
+ if TYPE_CHECKING:
+ runtime_result = client.Runtime.evaluate(expression="1 + 1", returnByValue=True)
+ assert_type(runtime_result, RuntimeDomain._EvaluateResult)
+
+ def on_target_created(event: TargetDomain._TargetCreatedEvent) -> None:
+ assert_type(event.targetId, str | None)
+
+ assert_type(client.on(client.Target.targetCreated, on_target_created), ModCDPClient)
+ assert_type(
+ client.Mod.addCustomCommand("Custom.echo", params_schema=CustomEchoParams, result_schema=CustomEchoResult),
+ AwaitableDict | AwaitableValue,
+ )
+ assert_type(client.Mod.addCustomEvent("Custom.ready", event_schema=CustomReadyEvent), AwaitableDict | AwaitableValue)
+ assert_type(
+ client.Mod.addMiddleware(
+ name="Target.getTargets",
+ phase="response",
+ expression="async (value, next) => next(value)",
+ ),
+ AwaitableDict | AwaitableValue,
+ )
+
+ runtime_params = {"expression": "1 + 1", "returnByValue": True}
+ runtime_result_payload = {"result": {"type": "number", "value": 2, "description": "2"}}
+ native_target_info: dict[str, JsonValue] = {
+ "targetId": "target-1",
+ "type": "page",
+ "title": "Example",
+ "url": "https://example.com",
+ "attached": False,
+ "canAccessOpener": False,
+ }
+ native_event_payload: ProtocolPayload = {"targetInfo": native_target_info}
+
+ self.assertEqual(client._validate_command_params("Runtime.evaluate", runtime_params), runtime_params)
+ self.assertEqual(client._validate_command_result("Runtime.evaluate", runtime_result_payload), runtime_result_payload)
+ self.assertEqual(client._validate_event_payload("Target.targetCreated", native_event_payload), native_event_payload)
+ with self.assertRaises(ValueError):
+ client._validate_command_params("Runtime.evaluate", {})
+ with self.assertRaises(ValueError):
+ client._validate_command_result("Runtime.evaluate", {})
+ with self.assertRaises(ValueError):
+ client._validate_event_payload("Target.targetCreated", {})
+
+ client.Mod.addCustomCommand("Custom.echo", params_schema=CustomEchoParams, result_schema=CustomEchoResult)
+ client.Mod.addCustomEvent("Custom.ready", event_schema=CustomReadyEvent)
+
+ self.assertEqual(client._validate_command_params("Custom.echo", {"text": "ok"}), {"text": "ok"})
+ self.assertEqual(client._validate_command_result("Custom.echo", {"ok": True}), True)
+ self.assertEqual(client._validate_event_payload("Custom.ready", {"ok": True}), CustomReadyEvent(ok=True))
+ with self.assertRaises(ValueError):
+ client._validate_command_params("Custom.echo", {"text": 1})
+ with self.assertRaises(ValueError):
+ client._validate_command_result("Custom.echo", {"ok": "yes"})
+ with self.assertRaises(ValueError):
+ client._validate_event_payload("Custom.ready", {"ok": "yes"})
+
+ client.Mod.addCustomCommand(
+ "Target.getTargets",
+ result_schema={
+ "type": "object",
+ "properties": {
+ "targetInfos": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "targetId": {"type": "string"},
+ "type": {"type": "string"},
+ "title": {"type": "string"},
+ "url": {"type": "string"},
+ "attached": {"type": "boolean"},
+ "canAccessOpener": {"type": "boolean"},
+ "tabId": {"type": "integer"},
+ },
+ "required": ["targetId", "type", "title", "url", "attached", "canAccessOpener"],
+ "additionalProperties": True,
+ },
+ }
+ },
+ "required": ["targetInfos"],
+ "additionalProperties": True,
+ },
+ )
+ client.Mod.addCustomEvent("Target.targetCreated")
+
+ extended_target_info = {**native_target_info, "tabId": 7}
+ self.assertEqual(client._validate_command_result("Target.getTargets", {"targetInfos": [extended_target_info]}), {"targetInfos": [extended_target_info]})
+ self.assertEqual(
+ client._validate_event_payload("Target.targetCreated", {"targetInfo": extended_target_info}),
+ {"targetInfo": extended_target_info},
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/python/tests/test_NativeMessagingUpstreamTransport.py b/python/tests/test_NativeMessagingUpstreamTransport.py
index 4c5f3e4..f11bdc9 100644
--- a/python/tests/test_NativeMessagingUpstreamTransport.py
+++ b/python/tests/test_NativeMessagingUpstreamTransport.py
@@ -3,14 +3,22 @@
import threading
import time
import unittest
+import glob
import os
+import re
import socket
+import sys
+import tempfile
from pathlib import Path
from queue import Queue
from modcdp import ModCDPClient
from modcdp.transport.NativeMessagingUpstreamTransport import NativeMessagingUpstreamTransport
+ROOT = Path(__file__).resolve().parents[2]
+EXTENSION_PATH = ROOT / "dist" / "extension"
+NATIVE_MESSAGING_TEST_BROWSER_PATH: str | None = None
+
class NativeMessagingUpstreamTransportTests(unittest.TestCase):
def test_config_owns_manifest_host_wait_timeout_loopback_and_injector_config(self) -> None:
@@ -157,11 +165,24 @@ def wait_for_peer() -> None:
def test_installs_launch_profile_native_host_manifest_and_connects_to_real_extension(self) -> None:
upstream_nativemessaging_host_name = "com.modcdp.bridge"
+ temp_profile_dir = tempfile.TemporaryDirectory(prefix="modcdp.native.")
cdp = ModCDPClient(
- launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}},
+ launcher={
+ "launcher_mode": "local",
+ "launcher_options": {
+ "headless": True,
+ "user_data_dir": temp_profile_dir.name,
+ "cleanup_user_data_dir": True,
+ # Native messaging is browser -> client only. After explicit CHROME_PATH
+ # and CI /usr/bin/chromium, this test uses Chrome for Testing because
+ # Canary rejects --load-extension in this local test path.
+ "executable_path": extension_launch_flag_test_browser_path(),
+ },
+ },
upstream={"upstream_mode": "nativemessaging", "upstream_nativemessaging_host_name": upstream_nativemessaging_host_name},
injector={
"injector_mode": "auto",
+ "injector_extension_path": str(EXTENSION_PATH),
"injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
"injector_trust_service_worker_target": True,
},
@@ -174,8 +195,8 @@ def test_installs_launch_profile_native_host_manifest_and_connects_to_real_exten
self.assertEqual(cdp.upstream_endpoint_kind, "modcdp_server")
transport_url = cdp.transport.url if cdp.transport and cdp.transport.url else ""
self.assertRegex(transport_url, rf"^native://{upstream_nativemessaging_host_name}@127\.0\.0\.1:\d+$")
- profile_dir = cdp._launched_browser.get("profile_dir") if cdp._launched_browser else ""
- self.assertTrue((Path(profile_dir) / "NativeMessagingHosts" / f"{upstream_nativemessaging_host_name}.json").exists())
+ launched_profile_dir = cdp._launched_browser.get("profile_dir") if cdp._launched_browser else ""
+ self.assertTrue((Path(launched_profile_dir) / "NativeMessagingHosts" / f"{upstream_nativemessaging_host_name}.json").exists())
version = cdp.send("Browser.getVersion")
self.assertIsInstance(version["product"], str)
time.sleep(1.5)
@@ -183,6 +204,7 @@ def test_installs_launch_profile_native_host_manifest_and_connects_to_real_exten
self.assertIsInstance(second_version["product"], str)
finally:
cdp.close()
+ temp_profile_dir.cleanup()
def _wait_until(predicate, timeout_s: float = 2.0) -> None:
@@ -194,5 +216,53 @@ def _wait_until(predicate, timeout_s: float = 2.0) -> None:
raise AssertionError("timed out waiting for condition")
+def extension_launch_flag_test_browser_path() -> str:
+ global NATIVE_MESSAGING_TEST_BROWSER_PATH
+ if NATIVE_MESSAGING_TEST_BROWSER_PATH is not None:
+ return NATIVE_MESSAGING_TEST_BROWSER_PATH
+ explicit_candidates = [
+ os.environ.get("CHROME_PATH"),
+ "/usr/bin/chromium" if sys.platform.startswith("linux") else None,
+ ]
+ for candidate in explicit_candidates:
+ if candidate and Path(candidate).exists():
+ NATIVE_MESSAGING_TEST_BROWSER_PATH = candidate
+ return candidate
+
+ home = Path.home()
+ if sys.platform == "darwin":
+ patterns = [
+ str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"),
+ str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"),
+ str(home / "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"),
+ ]
+ elif sys.platform.startswith("win"):
+ local_app_data = Path(os.environ.get("LOCALAPPDATA") or home / "AppData/Local")
+ patterns = [
+ str(local_app_data / "ms-playwright/chromium-*/chrome-win*/chrome.exe"),
+ str(home / ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"),
+ ]
+ else:
+ patterns = [
+ str(home / ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"),
+ "/opt/pw-browsers/chromium-*/chrome-linux*/chrome",
+ str(home / ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"),
+ ]
+ candidates = sorted(
+ {candidate for pattern in patterns for candidate in glob.glob(pattern)},
+ key=lambda candidate: (_path_version(candidate), Path(candidate).stat().st_mtime),
+ reverse=True,
+ )
+ if candidates:
+ NATIVE_MESSAGING_TEST_BROWSER_PATH = candidates[0]
+ return candidates[0]
+ raise RuntimeError("Native messaging tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.")
+
+
+def _path_version(candidate: str) -> int:
+ numbers = [int(value) for value in re.findall(r"\d+", candidate)]
+ return max(numbers) if numbers else 0
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/python/tests/test_PipeUpstreamTransport.py b/python/tests/test_PipeUpstreamTransport.py
index 25f9553..6221984 100644
--- a/python/tests/test_PipeUpstreamTransport.py
+++ b/python/tests/test_PipeUpstreamTransport.py
@@ -2,10 +2,14 @@
import os
import unittest
+from pathlib import Path
from modcdp import ModCDPClient
from modcdp.transport.PipeUpstreamTransport import PipeUpstreamTransport
+ROOT = Path(__file__).resolve().parents[2]
+EXTENSION_PATH = ROOT / "dist" / "extension"
+
class PipeUpstreamTransportTests(unittest.TestCase):
def test_constructor_update_launcher_config_and_unconnected_errors_match_transport_surface(self) -> None:
@@ -19,7 +23,7 @@ def test_constructor_update_launcher_config_and_unconnected_errors_match_transpo
with self.assertRaisesRegex(RuntimeError, r"upstream\.upstream_mode=pipe requires"):
transport.connect()
with self.assertRaisesRegex(RuntimeError, "CDP pipe is not connected"):
- transport.send({"id": 1, "method": "Browser.getVersion"})
+ transport.send({"id": 1, "method": "Runtime.evaluate"})
def test_resets_connection_state_after_pipe_closes(self) -> None:
read_fd, read_writer_fd = os.pipe()
@@ -34,24 +38,26 @@ def test_resets_connection_state_after_pipe_closes(self) -> None:
try:
transport.connect()
- transport.send({"id": 1, "method": "Browser.getVersion", "params": {}})
+ transport.send({"id": 1, "method": "Runtime.evaluate", "params": {"expression": "1"}})
pipe_read_writer.close()
self.assertTrue(_wait_for(lambda: len(closed) == 1))
with self.assertRaisesRegex(RuntimeError, "CDP pipe is not connected"):
- transport.send({"id": 2, "method": "Browser.getVersion", "params": {}})
+ transport.send({"id": 2, "method": "Runtime.evaluate", "params": {"expression": "1"}})
finally:
transport.close()
pipe_write_reader.close()
def test_launches_real_browser_and_uses_pid_scoped_pipe_url(self) -> None:
cdp = ModCDPClient(
- launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}},
+ launcher={"launcher_mode": "local", "launcher_options": {"headless": True}},
upstream={"upstream_mode": "pipe"},
injector={
- "injector_mode": "auto",
+ "injector_mode": "inject",
+ "injector_extension_path": str(EXTENSION_PATH),
"injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
"injector_trust_service_worker_target": True,
},
+ server={"server_routes": {"*.*": "chrome_debugger"}},
)
try:
@@ -60,8 +66,12 @@ def test_launches_real_browser_and_uses_pid_scoped_pipe_url(self) -> None:
self.assertEqual(cdp.upstream_endpoint_kind, "raw_cdp")
self.assertRegex(cdp.cdp_url or "", r"^pipe://\d+$")
self.assertEqual(cdp.transport.url if cdp.transport else None, cdp.cdp_url)
- version = cdp.sendRaw("Browser.getVersion")
- self.assertIsInstance(version["product"], str)
+ cdp.Mod.addCustomCommand(
+ "Custom.runtimeReadyState",
+ expression="async () => await cdp.send('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true })",
+ )
+ runtime = cdp.send("Custom.runtimeReadyState")
+ self.assertEqual(runtime["result"]["value"], "complete")
finally:
cdp.close()
diff --git a/python/tests/test_RemoteBrowserLauncher.py b/python/tests/test_RemoteBrowserLauncher.py
index 9962416..e4d60a5 100644
--- a/python/tests/test_RemoteBrowserLauncher.py
+++ b/python/tests/test_RemoteBrowserLauncher.py
@@ -17,7 +17,7 @@ def test_requires_upstream_cdp_url(self) -> None:
def test_connects_to_real_browser_from_http_and_websocket_cdp_endpoints(self) -> None:
port = LocalBrowserLauncher.freePort()
local = LocalBrowserLauncher().launch(
- {"port": port, "headless": True, "sandbox": False, "chrome_ready_timeout_ms": 45_000}
+ {"port": port, "headless": True, "chrome_ready_timeout_ms": 45_000}
)
ws = None
try:
diff --git a/python/tests/test_ReverseWebSocketUpstreamTransport.py b/python/tests/test_ReverseWebSocketUpstreamTransport.py
index 85a5f0e..734ad29 100644
--- a/python/tests/test_ReverseWebSocketUpstreamTransport.py
+++ b/python/tests/test_ReverseWebSocketUpstreamTransport.py
@@ -1,12 +1,15 @@
from __future__ import annotations
import json
+import glob
import os
+import re
import socket
import sys
import threading
import time
import unittest
+from pathlib import Path
from queue import Queue
from typing import cast
@@ -15,15 +18,19 @@
from modcdp import ModCDPClient
from modcdp.transport.ReverseWebSocketUpstreamTransport import ReverseWebSocketUpstreamTransport
+ROOT = Path(__file__).resolve().parents[2]
+EXTENSION_PATH = ROOT / "dist" / "extension"
+REVERSEWS_TEST_BROWSER_PATH = None
+
class ReverseWebSocketUpstreamTransportTests(unittest.TestCase):
- def test_config_owns_bind_updates_wait_timeout_and_injector_config(self) -> None:
+ def test_config_owns_bind_updates_and_wait_timeout(self) -> None:
transport = ReverseWebSocketUpstreamTransport({
"upstream_reversews_bind": "127.0.0.1:29292",
"upstream_reversews_wait_timeout_ms": 10,
})
self.assertEqual(transport.url, "ws://127.0.0.1:29292")
- self.assertEqual(transport.getInjectorConfig(), {"upstream_reversews_url": "ws://127.0.0.1:29292"})
+ self.assertEqual(transport.getInjectorConfig(), {})
self.assertIs(
transport.update({
"upstream_reversews_bind": "127.0.0.1:29293",
@@ -32,7 +39,7 @@ def test_config_owns_bind_updates_wait_timeout_and_injector_config(self) -> None
transport,
)
self.assertEqual(transport.url, "ws://127.0.0.1:29293")
- self.assertEqual(transport.getInjectorConfig(), {"upstream_reversews_url": "ws://127.0.0.1:29293"})
+ self.assertEqual(transport.getInjectorConfig(), {})
with self.assertRaisesRegex(RuntimeError, "Timed out waiting 5ms"):
transport.waitForPeer()
@@ -134,19 +141,26 @@ def test_accepts_replacement_peer_after_disconnect(self) -> None:
first_peer.close()
transport.close()
- def test_accepts_real_extension_reverse_connection_and_routes_cdp_through_loopback(self) -> None:
+ def test_accepts_real_extension_reverse_connection_and_routes_cdp_through_chrome_debugger(self) -> None:
cdp = ModCDPClient(
launcher={
"launcher_mode": "local",
- "launcher_options": {"headless": sys.platform.startswith("linux") and not os.environ.get("DISPLAY"), "sandbox": False},
+ "launcher_options": {
+ "headless": sys.platform.startswith("linux") and not os.environ.get("DISPLAY"),
+ # Reversews is browser -> client only. After explicit CHROME_PATH and
+ # CI /usr/bin/chromium, these tests use Chrome for Testing because
+ # Canary rejects --load-extension in this local test path.
+ "executable_path": reversews_test_browser_path(),
+ },
},
upstream={"upstream_mode": "reversews"},
injector={
"injector_mode": "auto",
+ "injector_extension_path": str(EXTENSION_PATH),
"injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"],
"injector_trust_service_worker_target": True,
+ "injector_service_worker_probe_timeout_ms": 1_000,
},
- server={"server_routes": {"*.*": "loopback_cdp"}},
)
try:
@@ -155,15 +169,16 @@ def test_accepts_real_extension_reverse_connection_and_routes_cdp_through_loopba
self.assertEqual(cdp.upstream_endpoint_kind, "modcdp_server")
self.assertIsInstance(cdp.transport, ReverseWebSocketUpstreamTransport)
transport = cast(ReverseWebSocketUpstreamTransport, cdp.transport)
+ self.assertEqual(transport.url, "ws://127.0.0.1:29292")
self.assertEqual(
transport.peer_info.get("extension_id") if transport.peer_info else None,
"mdedooklbnfejodmnhmkdpkaedafkehf",
)
- version = cdp.send("Browser.getVersion")
- self.assertIsInstance(version["product"], str)
+ evaluated = cdp.send("Runtime.evaluate", {"expression": "location.href", "returnByValue": True})
+ self.assertEqual(evaluated["result"]["value"], "about:blank")
time.sleep(1.5)
- second_version = cdp.send("Browser.getVersion")
- self.assertIsInstance(second_version["product"], str)
+ second_evaluated = cdp.send("Runtime.evaluate", {"expression": "document.readyState", "returnByValue": True})
+ self.assertEqual(second_evaluated["result"]["value"], "complete")
finally:
cdp.close()
@@ -186,5 +201,57 @@ def _wait_until(predicate, timeout_s: float = 2.0) -> None:
raise AssertionError("timed out waiting for condition")
+def reversews_test_browser_path() -> str:
+ global REVERSEWS_TEST_BROWSER_PATH
+ if REVERSEWS_TEST_BROWSER_PATH is not None:
+ return REVERSEWS_TEST_BROWSER_PATH
+ explicit_candidates = [
+ os.environ.get("CHROME_PATH"),
+ "/usr/bin/chromium" if sys.platform.startswith("linux") else None,
+ ]
+ for candidate in explicit_candidates:
+ if candidate and Path(candidate).exists():
+ REVERSEWS_TEST_BROWSER_PATH = candidate
+ return candidate
+ home = Path.home()
+ if sys.platform == "darwin":
+ patterns = [
+ str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"),
+ str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"),
+ str(home / "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"),
+ ]
+ elif sys.platform.startswith("win"):
+ local_app_data = Path(os.environ.get("LOCALAPPDATA") or home / "AppData/Local")
+ patterns = [
+ str(local_app_data / "ms-playwright/chromium-*/chrome-win*/chrome.exe"),
+ str(home / ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"),
+ ]
+ else:
+ patterns = [
+ str(home / ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"),
+ "/opt/pw-browsers/chromium-*/chrome-linux*/chrome",
+ str(home / ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"),
+ ]
+ candidates = newest_first([match for pattern in patterns for match in glob.glob(pattern)])
+ if candidates:
+ REVERSEWS_TEST_BROWSER_PATH = candidates[0]
+ return candidates[0]
+ raise RuntimeError("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.")
+
+
+def newest_first(candidates: list[str]) -> list[str]:
+ return sorted(dict.fromkeys(candidates), key=path_score)
+
+
+def path_score(candidate: str) -> tuple[int, float, str]:
+ numbers = [int(part) for part in re.findall(r"\d+", candidate)]
+ version = max(numbers) if numbers else 0
+ try:
+ mtime = Path(candidate).stat().st_mtime
+ except OSError:
+ mtime = 0.0
+ return (-version, -mtime, candidate)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/python/tests/test_WebSocketUpstreamTransport.py b/python/tests/test_WebSocketUpstreamTransport.py
index a1d524e..f576956 100644
--- a/python/tests/test_WebSocketUpstreamTransport.py
+++ b/python/tests/test_WebSocketUpstreamTransport.py
@@ -68,7 +68,7 @@ def test_constructor_update_and_server_config_match_ts_shape(self) -> None:
def test_launches_real_browser_and_speaks_raw_cdp(self) -> None:
cdp = ModCDPClient(
- launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}},
+ launcher={"launcher_mode": "local", "launcher_options": {"headless": True}},
upstream={"upstream_mode": "ws"},
injector={
"injector_mode": "auto",
@@ -113,7 +113,7 @@ def test_launches_real_browser_and_speaks_raw_cdp(self) -> None:
cdp.close()
def test_resolves_real_http_cdp_endpoint_to_browser_websocket(self) -> None:
- chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch()
+ chrome = LocalBrowserLauncher({"headless": True}).launch()
transport = WebSocketUpstreamTransport({"cdp_url": chrome["cdp_url"]})
received: Queue[dict] = Queue()
transport.onRecv(lambda message: received.put(message))
@@ -130,7 +130,7 @@ def test_resolves_real_http_cdp_endpoint_to_browser_websocket(self) -> None:
def test_resolves_real_host_port_cdp_endpoint_to_browser_websocket(self) -> None:
port = LocalBrowserLauncher.freePort()
- chrome = LocalBrowserLauncher({"port": port, "headless": True, "sandbox": False}).launch()
+ chrome = LocalBrowserLauncher({"port": port, "headless": True}).launch()
transport = WebSocketUpstreamTransport({"cdp_url": f"127.0.0.1:{port}"})
received: Queue[dict] = Queue()
transport.onRecv(lambda message: received.put(message))
@@ -146,7 +146,7 @@ def test_resolves_real_host_port_cdp_endpoint_to_browser_websocket(self) -> None
chrome["close"]()
def test_close_clears_connection_state(self) -> None:
- chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch()
+ chrome = LocalBrowserLauncher({"headless": True}).launch()
transport = WebSocketUpstreamTransport({"cdp_url": chrome["cdp_url"]})
try:
diff --git a/python/tests/test_translate.py b/python/tests/test_translate.py
index 3fa640b..fd9ff1c 100644
--- a/python/tests/test_translate.py
+++ b/python/tests/test_translate.py
@@ -2,6 +2,7 @@
import unittest
import json
+from typing import cast
from modcdp.translate import (
CUSTOM_EVENT_BINDING_NAME,
@@ -38,6 +39,19 @@ def test_routes_wraps_and_unwraps_modcdp_protocol_messages_deterministically(sel
)
self.assertEqual(configured["steps"][0].get("unwrap"), "runtime_json")
+ custom = wrap_command_if_needed(
+ "Custom.echo",
+ {"secret": "x" * 100, "nested": {"ok": True}},
+ cdp_session_id="session-1",
+ )
+ custom_step_params = custom["steps"][0].get("params", {})
+ self.assertIn("JSON.parse(paramsJson)", str(custom_step_params.get("functionDeclaration")))
+ self.assertNotIn("xxxxxxxxxx", str(custom_step_params.get("functionDeclaration")))
+ custom_arguments = cast("list[dict[str, object]]", custom_step_params.get("arguments", []))
+ self.assertEqual(custom_arguments[0].get("value"), "Custom.echo")
+ self.assertEqual(json.loads(str(custom_arguments[1].get("value"))), {"secret": "x" * 100, "nested": {"ok": True}})
+ self.assertEqual(custom_arguments[2].get("value"), "session-1")
+
self.assertEqual(unwrap_response_if_needed({"result": {"type": "object", "value": {"ok": True}}}, "runtime"), {"ok": True})
self.assertEqual(unwrap_response_if_needed({"product": "Chrome/1"}, None), {"product": "Chrome/1"})
diff --git a/python/uv.lock b/python/uv.lock
index 1097ad8..68c2ac9 100644
--- a/python/uv.lock
+++ b/python/uv.lock
@@ -13,7 +13,7 @@ wheels = [
[[package]]
name = "modcdp"
-version = "0.0.13"
+version = "0.0.19"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },
diff --git a/tsconfig.json b/tsconfig.json
index 6d66b2b..fa02b7e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,6 +20,6 @@
"useUnknownInCatchVariables": false,
"verbatimModuleSyntax": true
},
- "include": ["js/src/**/*.ts", "js/examples/**/*.ts", "extension/src/**/*.ts"],
+ "include": ["js/src/**/*.ts", "js/examples/**/*.ts", "js/test/**/*.ts", "extension/src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}