aboutsummaryrefslogtreecommitdiff
path: root/test-suite
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-08 15:19:35 +0000
committerIgor Soarez <igor@soarez.org>2026-11-08 15:19:35 +0000
commit4d353df1a83e36b4e6b2edb0ba64190423e4a7f3 (patch)
tree035e1f80347c5664105c265338e80ebfd2193af4 /test-suite
parent9004966da6491b368d12abeeb175256eedb78a2a (diff)
overlay: tell the agent about a blocking password-manager prompt; chrome_clear_overlay closes it on request
Another extension's frame (1Password's inline prompt) makes Chrome refuse the debugger on the tab. The old path sent Escape on its own, which cannot reach that frame and would close a prompt the user may be using. Now nothing is closed automatically: the error says what is blocking and tells the agent to ask the user; chrome_clear_overlay removes the frame, blurs the field, sends Escape and reports whether the tab is usable again.
Diffstat (limited to 'test-suite')
-rw-r--r--test-suite/unit/overlay.test.mjs91
1 files changed, 91 insertions, 0 deletions
diff --git a/test-suite/unit/overlay.test.mjs b/test-suite/unit/overlay.test.mjs
new file mode 100644
index 0000000..de18da8
--- /dev/null
+++ b/test-suite/unit/overlay.test.mjs
@@ -0,0 +1,91 @@
+// Another extension's prompt (1Password) blocks the debugger on a tab. We never close it on our
+// own, because the user may be using it; the agent is told, and chrome_clear_overlay closes it
+// when asked.
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import vm from "node:vm";
+import { test } from "node:test";
+
+const workerSource = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/browser-extension/service_worker.js", import.meta.url), "utf8");
+const indexSource = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/index.ts", import.meta.url), "utf8");
+
+const BLOCK = "Cannot access a chrome-extension:// URL of different extension";
+const frame = { id: "F", tabId: 2, type: "other", url: "chrome-extension://aeblf/inline/universal-sign-on/x.html?tabId=2#t", extensionId: undefined };
+
+function worker({ targets = [frame], clears = true, scriptResult = { removed: ["chrome-extension://aeblf/inline/universal-sign-on/x.html"] } } = {}) {
+ const calls = [];
+ const listener = { addListener() {}, removeListener() {} };
+ let cleared = false;
+ const w = {
+ console, setTimeout, clearTimeout, setInterval: () => 0, navigator: { userAgent: "unit-test" }, fetch: async () => { throw new Error("offline"); },
+ chrome: {
+ runtime: { id: "ours", getManifest: () => ({ version: "0.0.0" }), onInstalled: listener, onStartup: listener },
+ alarms: { create() {}, onAlarm: listener }, action: { onClicked: listener }, webNavigation: { onCommitted: listener },
+ debugger: { onDetach: listener, onEvent: listener, getTargets: (cb) => cb(cleared ? [] : targets) },
+ storage: { session: { get: async () => ({}), set: async () => {} } },
+ scripting: { executeScript: async (options) => { calls.push({ method: "scripting", options }); if (clears) cleared = true; return [{ result: scriptResult }]; } },
+ tabs: { get: async () => ({ id: 2, url: "https://news.ycombinator.com/login", title: "login" }), query: async () => [{ id: 2, url: "https://news.ycombinator.com/login", title: "login" }] },
+ },
+ };
+ w.self = w;
+ vm.runInNewContext(workerSource, w);
+ w.sleep = async () => {};
+ w.getTabByParams = async () => ({ id: 2, url: "https://news.ycombinator.com/login", title: "login" });
+ w.attachDebugger = async () => { if (!cleared) throw new Error(BLOCK); };
+ return { w, calls, isCleared: () => cleared };
+}
+
+test("a blocked input is not dismissed with Escape: the agent is told to ask the user", async () => {
+ const { w, calls } = worker();
+ w.cdpRaw = async (_tab, method) => { calls.push({ method }); throw new Error(`${method}: ${BLOCK}`); };
+ await assert.rejects(
+ () => w.cdp(2, "Input.dispatchMouseEvent", {}),
+ (e) => /may be using it right now/.test(e.message) && /ask the user/.test(e.message) && /chrome_clear_overlay/.test(e.message) && /aeblf/.test(e.message),
+ );
+ assert.deepEqual(calls.map((c) => c.method), ["Input.dispatchMouseEvent"]);
+});
+
+test("overlay.clear removes the frame and reports the tab usable", async () => {
+ const { w, calls } = worker();
+ const result = await w.dispatch("overlay.clear", { targetId: "2" });
+ assert.equal(result.cleared, true);
+ assert.deepEqual(result.removedFrames, ["chrome-extension://aeblf/inline/universal-sign-on/x.html"]);
+ assert.equal(result.debuggerAttachable, true);
+ assert.equal(calls[0].options.args[0], "ours");
+});
+
+test("overlay.clear says so when the tab is still blocked (toolbar popup, desktop app)", async () => {
+ const { w } = worker({ clears: false, scriptResult: { removed: [] } });
+ const result = await w.dispatch("overlay.clear", { targetId: "2" });
+ assert.equal(result.cleared, false);
+ assert.equal(result.debuggerAttachable, false);
+ assert.deepEqual(result.remainingForeignTargets, ["chrome-extension://aeblf/inline/universal-sign-on/x.html"]);
+});
+
+test("the injected clean-up removes foreign extension iframes, not ours, and blurs the field", () => {
+ const { w } = worker();
+ const mk = (src) => ({ tagName: "IFRAME", src, removed: false, remove() { this.removed = true; } });
+ const foreign = mk("chrome-extension://aeblf/inline/a.html?x=1");
+ const ours = mk("chrome-extension://ours/panel.html");
+ const page = mk("https://example.com/embed");
+ const shadowFrame = mk("chrome-extension://aeblf/inline/b.html");
+ const host = { tagName: "DIV", shadowRoot: { querySelectorAll: () => [shadowFrame] } };
+ let blurred = false, escaped = false;
+ const sandbox = {
+ document: { querySelectorAll: () => [foreign, ours, page, host], activeElement: { blur: () => { blurred = true; } }, body: {}, dispatchEvent: (e) => { escaped = e.key === "Escape"; } },
+ KeyboardEvent: class { constructor(type, init) { this.type = type; Object.assign(this, init); } },
+ Array,
+ };
+ const result = vm.runInNewContext(`(${w.clearForeignOverlays.toString()})("ours")`, sandbox);
+ assert.deepEqual([...result.removed], ["chrome-extension://aeblf/inline/a.html", "chrome-extension://aeblf/inline/b.html"]);
+ assert.equal(foreign.removed && shadowFrame.removed, true);
+ assert.equal(ours.removed || page.removed, false);
+ assert.ok(blurred && escaped);
+});
+
+test("the tool tells the agent not to call it on its own, and the primer says the same", () => {
+ assert.match(indexSource, /name: "chrome_clear_overlay"/);
+ assert.match(indexSource, /So never call this on your own: ask the user first/);
+ assert.match(indexSource, /11\. If a tool fails saying another extension/);
+ assert.match(indexSource, /"chrome_clear_overlay",\n\] as const/);
+});