diff options
| author | Igor Soarez <igor@soarez.org> | 2026-11-08 15:19:35 +0000 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-11-08 15:19:35 +0000 |
| commit | 4d353df1a83e36b4e6b2edb0ba64190423e4a7f3 (patch) | |
| tree | 035e1f80347c5664105c265338e80ebfd2193af4 /test-suite/unit | |
| parent | 9004966da6491b368d12abeeb175256eedb78a2a (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/unit')
| -rw-r--r-- | test-suite/unit/overlay.test.mjs | 91 |
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/); +}); |
