Skip to content

Commit 9a77e34

Browse files
committed
Use real browser for absent CLI extension target test
1 parent 3fada3c commit 9a77e34

3 files changed

Lines changed: 160 additions & 63 deletions

File tree

go/modcdp/injector/CLIExtensionInjector_test.go

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ package injector_test
99
import (
1010
"archive/zip"
1111
. "github.com/browserbase/modcdp/go/modcdp/injector"
12+
"github.com/browserbase/modcdp/go/modcdp/launcher"
13+
"github.com/browserbase/modcdp/go/modcdp/transport"
1214
"os"
1315
"path/filepath"
1416
"strings"
1517
"testing"
16-
"time"
1718
)
1819

20+
const doesNotExistExtensionID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
21+
1922
func TestCLIExtensionInjectorRejectsZipEntriesOutsideExtractionDirectory(t *testing.T) {
2023
tempDir := t.TempDir()
2124
zipPath := filepath.Join(tempDir, "extension.zip")
@@ -99,48 +102,67 @@ func TestCLIExtensionInjectorPreparesTheDefaultExtensionZipForLoadExtension(t *t
99102
}
100103
}
101104

102-
func TestCLIExtensionInjectorReturnsImmediatelyWhenTheLaunchedExtensionTargetIsAbsent(t *testing.T) {
105+
func TestCLIExtensionInjectorReturnsNullWhenATrustedDoesNotExistExtensionIDIsAbsentInARealBrowser(t *testing.T) {
103106
extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension"))
104107
if err != nil {
105108
t.Fatal(err)
106109
}
107-
methods := []string{}
110+
headless := true
108111
injector := NewCLIExtensionInjector(InjectorConfig{
109112
InjectorCLIExtensionPath: extensionPath,
113+
InjectorCLIExtensionID: doesNotExistExtensionID,
110114
InjectorTrustServiceWorkerTarget: true,
111-
InjectorServiceWorkerReadyTimeoutMS: 50,
112-
InjectorServiceWorkerPollIntervalMS: 10,
113-
Send: func(method string, params map[string]any, sessionID string) (map[string]any, error) {
114-
methods = append(methods, method)
115-
if method == "Target.getTargets" {
116-
return map[string]any{"targetInfos": []any{}}, nil
117-
}
118-
t.Fatalf("unexpected %s", method)
119-
return nil, nil
120-
},
115+
InjectorServiceWorkerReadyTimeoutMS: 250,
116+
InjectorServiceWorkerPollIntervalMS: 25,
121117
})
122118
if err := injector.Prepare(); err != nil {
123119
t.Fatal(err)
124120
}
125121
defer injector.Close()
126122

127-
startedAt := time.Now()
123+
browserLauncher := launcher.NewLocalBrowserLauncher(launcher.LauncherConfig{
124+
LauncherLocalHeadless: &headless,
125+
LauncherLocalExecutablePath: loadExtensionTestBrowserPath(t),
126+
})
127+
browserLauncher.Update(injector.ConfigForLauncher())
128+
if _, err := browserLauncher.Launch(launcher.LauncherConfig{}); err != nil {
129+
t.Fatal(err)
130+
}
131+
defer browserLauncher.Close()
132+
133+
upstream := transport.NewWSUpstreamTransport(transport.UpstreamTransportConfig{})
134+
upstream.Update(browserLauncher.ConfigForUpstream())
135+
if err := upstream.Connect(); err != nil {
136+
t.Fatal(err)
137+
}
138+
defer upstream.Close()
139+
injector.Update(InjectorConfig{
140+
Send: func(method string, params map[string]any, sessionID string) (map[string]any, error) {
141+
return upstream.Send(method, params, sessionID)
142+
},
143+
})
144+
145+
targets, err := upstream.Send("Target.getTargets", map[string]any{}, "")
146+
if err != nil {
147+
t.Fatal(err)
148+
}
149+
targetInfos, _ := targets["targetInfos"].([]any)
150+
for _, rawTarget := range targetInfos {
151+
target, _ := rawTarget.(map[string]any)
152+
if target == nil {
153+
continue
154+
}
155+
targetURL, _ := target["url"].(string)
156+
if strings.HasPrefix(targetURL, "chrome-extension://"+doesNotExistExtensionID+"/") {
157+
t.Fatalf("found does-not-exist extension target: %#v", target)
158+
}
159+
}
160+
128161
result, err := injector.Inject()
129162
if err != nil {
130163
t.Fatal(err)
131164
}
132-
elapsed := time.Since(startedAt)
133165
if result != nil {
134166
t.Fatalf("result = %#v", result)
135167
}
136-
uniqueMethods := map[string]bool{}
137-
for _, method := range methods {
138-
uniqueMethods[method] = true
139-
}
140-
if len(methods) == 0 || len(uniqueMethods) != 1 || !uniqueMethods["Target.getTargets"] {
141-
t.Fatalf("methods = %#v", methods)
142-
}
143-
if elapsed >= 200*time.Millisecond {
144-
t.Fatalf("Inject took %s", elapsed)
145-
}
146168
}

js/test/test.CLIExtensionInjector.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ import { test } from "vitest";
1313

1414
import { DEFAULT_MODCDP_EXTENSION_ID } from "../src/injector/ExtensionInjector.js";
1515
import { CLIExtensionInjector } from "../src/injector/CLIExtensionInjector.js";
16+
import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js";
17+
import { WSUpstreamTransport } from "../src/transport/WSUpstreamTransport.js";
18+
import { loadExtensionTestBrowserPath } from "./browserPaths.js";
1619

1720
const HERE = path.dirname(fileURLToPath(import.meta.url));
1821
const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension");
22+
const DOES_NOT_EXIST_EXTENSION_ID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
23+
const LOAD_EXTENSION_TEST_BROWSER_PATH = loadExtensionTestBrowserPath();
1924

2025
function crc32(data: Buffer) {
2126
let crc = 0xffffffff;
@@ -107,31 +112,43 @@ test("CLIExtensionInjector prepares the default extension zip for --load-extensi
107112
}
108113
});
109114

110-
test("CLIExtensionInjector returns immediately when the launched extension target is absent", async () => {
111-
const methods: string[] = [];
115+
test("CLIExtensionInjector returns null when a trusted does-not-exist extension id is absent in a real browser", async () => {
112116
const injector = new CLIExtensionInjector({
113117
injector_cli_extension_path: EXTENSION_PATH,
118+
injector_cli_extension_id: DOES_NOT_EXIST_EXTENSION_ID,
114119
injector_trust_service_worker_target: true,
115-
injector_service_worker_ready_timeout_ms: 50,
116-
injector_service_worker_poll_interval_ms: 10,
117-
send: async (method) => {
118-
const method_name = typeof method === "string" ? method : method.id;
119-
methods.push(method_name);
120-
if (method_name === "Target.getTargets") return { targetInfos: [] };
121-
throw new Error(`unexpected ${method_name}`);
122-
},
120+
injector_service_worker_ready_timeout_ms: 250,
121+
injector_service_worker_poll_interval_ms: 25,
123122
});
123+
const launcher = new LocalBrowserLauncher({
124+
launcher_local_headless: true,
125+
launcher_local_executable_path: LOAD_EXTENSION_TEST_BROWSER_PATH,
126+
});
127+
const upstream = new WSUpstreamTransport();
124128

125129
try {
126130
await injector.prepare();
127-
const started_at = performance.now();
131+
launcher.update(injector.configForLauncher());
132+
await launcher.launch();
133+
upstream.update(launcher.configForUpstream());
134+
await upstream.connect();
135+
injector.update({ send: upstream.send.bind(upstream) });
136+
137+
const targets = (await upstream.send("Target.getTargets", {})) as {
138+
targetInfos?: { type?: string; url?: string }[];
139+
};
140+
assert.equal(
141+
targets.targetInfos?.some((target) =>
142+
target.url?.startsWith(`chrome-extension://${DOES_NOT_EXIST_EXTENSION_ID}/`),
143+
),
144+
false,
145+
);
146+
128147
const result = await injector.inject();
129-
const elapsed_ms = performance.now() - started_at;
130148
assert.equal(result, null);
131-
assert.equal(methods.length > 0, true);
132-
assert.deepEqual([...new Set(methods)], ["Target.getTargets"]);
133-
assert.equal(elapsed_ms < 200, true, `inject() took ${elapsed_ms}ms`);
134149
} finally {
150+
await upstream.close();
151+
await launcher.close();
135152
await injector.close();
136153
}
137-
});
154+
}, 60_000);

python/tests/test_CLIExtensionInjector.py

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,63 @@
66
# USE REAL USER-FACING CODE PATHS WITH REAL BROWSERS, REAL CLASSES, REAL URLS, etc. Hard fail if keys or other env requirements are missing.
77
from __future__ import annotations
88

9-
import unittest
10-
import time
9+
import glob
10+
import os
11+
import re
12+
import sys
1113
import tempfile
14+
import unittest
1215
import zipfile
16+
from collections.abc import Mapping
1317
from pathlib import Path
14-
from typing import Any, cast
18+
from typing import cast
1519

1620
from modcdp.injector.ExtensionInjector import DEFAULT_MODCDP_EXTENSION_ID
1721
from modcdp.injector.CLIExtensionInjector import CLIExtensionInjector
22+
from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher
23+
from modcdp.transport.WSUpstreamTransport import WSUpstreamTransport
1824

1925

2026
ROOT = Path(__file__).resolve().parents[2]
2127
EXTENSION_PATH = ROOT / "dist" / "extension"
28+
DOES_NOT_EXIST_EXTENSION_ID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
29+
30+
31+
# MODCDP_TEST_SUPPORT: LANGUAGE-SPECIFIC TEST SUPPORT ONLY.
32+
# Keep setup semantics 1:1 with TS; this only selects a real browser for real --load-extension runs.
33+
def load_extension_test_browser_path() -> str:
34+
for candidate in (os.environ.get("CHROME_PATH"), "/usr/bin/chromium" if sys.platform.startswith("linux") else None):
35+
if candidate and Path(candidate).exists():
36+
return candidate
37+
home = Path.home()
38+
if sys.platform == "darwin":
39+
patterns = [
40+
str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"),
41+
str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"),
42+
str(home / "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"),
43+
]
44+
elif sys.platform.startswith("win"):
45+
local_app_data = Path(os.environ.get("LOCALAPPDATA") or home / "AppData/Local")
46+
patterns = [
47+
str(local_app_data / "ms-playwright/chromium-*/chrome-win*/chrome.exe"),
48+
str(home / ".cache/puppeteer/chrome/win*-*/chrome.exe"),
49+
]
50+
else:
51+
patterns = [
52+
str(home / ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"),
53+
"/opt/pw-browsers/chromium-*/chrome-linux*/chrome",
54+
str(home / ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"),
55+
]
56+
candidates = sorted(
57+
dict.fromkeys(match for pattern in patterns for match in glob.glob(pattern)),
58+
key=lambda path: (-max([int(part) for part in re.findall(r"\d+", path)] or [0]), -Path(path).stat().st_mtime, path),
59+
)
60+
if candidates:
61+
return candidates[0]
62+
raise RuntimeError("No browser found for --load-extension tests. Install Chrome for Testing or set CHROME_PATH.")
63+
64+
65+
LOAD_EXTENSION_TEST_BROWSER_PATH = load_extension_test_browser_path()
2266

2367

2468
class CLIExtensionInjectorTests(unittest.TestCase):
@@ -64,34 +108,48 @@ def test_cliextensioninjector_prepares_the_default_extension_zip_for_load_extens
64108
finally:
65109
injector.close()
66110

67-
def test_cliextensioninjector_returns_immediately_when_the_launched_extension_target_is_absent(self) -> None:
68-
methods: list[str] = []
69-
70-
def send(method: str, params: dict[str, Any] | None = None, session_id: str | None = None) -> dict[str, Any]:
71-
methods.append(method)
72-
if method == "Target.getTargets":
73-
return {"targetInfos": []}
74-
raise RuntimeError(f"unexpected {method}")
75-
111+
def test_cliextensioninjector_returns_null_when_a_trusted_does_not_exist_extension_id_is_absent_in_a_real_browser(self) -> None:
76112
injector = CLIExtensionInjector(
77-
cast(Any, {
113+
{
78114
"injector_cli_extension_path": str(EXTENSION_PATH),
115+
"injector_cli_extension_id": DOES_NOT_EXIST_EXTENSION_ID,
79116
"injector_trust_service_worker_target": True,
80-
"injector_service_worker_ready_timeout_ms": 50,
81-
"injector_service_worker_poll_interval_ms": 10,
82-
"send": send,
83-
})
117+
"injector_service_worker_ready_timeout_ms": 250,
118+
"injector_service_worker_poll_interval_ms": 25,
119+
}
120+
)
121+
launcher = LocalBrowserLauncher(
122+
{
123+
"launcher_local_headless": True,
124+
"launcher_local_executable_path": LOAD_EXTENSION_TEST_BROWSER_PATH,
125+
}
84126
)
127+
upstream = WSUpstreamTransport()
85128
try:
86129
injector.prepare()
87-
started_at = time.perf_counter()
130+
launcher.update(injector.configForLauncher())
131+
launcher.launch()
132+
upstream.update(launcher.configForUpstream())
133+
upstream.connect()
134+
injector.update({"send": upstream.send})
135+
136+
targets = upstream.send("Target.getTargets", {})
137+
target_infos = targets.get("targetInfos") if isinstance(targets, Mapping) else None
138+
if not isinstance(target_infos, list):
139+
raise AssertionError(f"Target.getTargets returned no targetInfos: {targets!r}")
140+
found_does_not_exist_target = False
141+
for target in target_infos:
142+
match target:
143+
case {"url": str(target_url)}:
144+
if target_url.startswith(f"chrome-extension://{DOES_NOT_EXIST_EXTENSION_ID}/"):
145+
found_does_not_exist_target = True
146+
self.assertFalse(found_does_not_exist_target)
147+
88148
result = injector.inject()
89-
elapsed_ms = (time.perf_counter() - started_at) * 1000
90149
self.assertIsNone(result)
91-
self.assertGreater(len(methods), 0)
92-
self.assertEqual(sorted(set(methods)), ["Target.getTargets"])
93-
self.assertLess(elapsed_ms, 200)
94150
finally:
151+
upstream.close()
152+
launcher.close()
95153
injector.close()
96154

97155

0 commit comments

Comments
 (0)