// Another extension's prompt (1Password) blocks the debugger on a tab. Nothing closes it // automatically, because the user may be using it; the agent is told what is blocking and decides: // chrome_clear_overlay when it is only in the way, leave it when it is waiting for the user. 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 and decides", 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) => /only in your way/.test(e.message) && /waiting for the user to sign in or approve/.test(e.message) && /leave it alone/.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"); assert.equal(calls[0].options.world, undefined); }); 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 a host whose closed shadow root holds a foreign frame, and loose foreign frames, but not ours or the page's", () => { const { w } = worker(); const frame = (src) => ({ tagName: "IFRAME", src, isConnected: true, removed: false, remove() { this.removed = true; } }); const foreign = frame("chrome-extension://aeblf/inline/a.html?x=1"); const ours = frame("chrome-extension://ours/panel.html"); const page = frame("https://example.com/embed"); const inClosedRoot = frame("chrome-extension://aeblf/inline/universal-sign-on.html?tabId=2"); const host = { tagName: "COM-1PASSWORD-USO", isConnected: true, removed: false, remove() { this.removed = true; }, shadowRoot: null, closedRoot: { querySelectorAll: () => [inClosedRoot] } }; const plain = { tagName: "DIV", isConnected: true, shadowRoot: null }; let blurred = false, escaped = false; const sandbox = { document: { querySelectorAll: (sel) => (sel === "iframe" ? [foreign, ours, page] : [foreign, ours, page, host, plain]), activeElement: { blur: () => { blurred = true; } }, body: {}, dispatchEvent: (e) => { escaped = e.key === "Escape"; } }, chrome: { dom: { openOrClosedShadowRoot: (el) => el.closedRoot ?? null } }, KeyboardEvent: class { constructor(type, init) { this.type = type; Object.assign(this, init); } }, Array, Set, }; const result = vm.runInNewContext(`(${w.clearForeignOverlays.toString()})("ours")`, sandbox); assert.deepEqual([...result.removed], ["chrome-extension://aeblf/inline/a.html", " holding chrome-extension://aeblf/inline/universal-sign-on.html"]); assert.ok(foreign.removed && host.removed); assert.equal(ours.removed || page.removed || inClosedRoot.removed, false); assert.ok(blurred && escaped); }); test("the tool and the primer let the agent decide: clear it when in the way, leave it when waiting for the user", () => { assert.match(indexSource, /name: "chrome_clear_overlay"/); assert.match(indexSource, /Decide for yourself whether to use it/); assert.match(indexSource, /do not clear it: tell the user what you are waiting for/); assert.match(indexSource, /11\. If a tool fails saying another extension[^`]*decide whether it is in your way or part of the task/); assert.match(indexSource, /"chrome_clear_overlay",\n\] as const/); });