diff options
Diffstat (limited to 'test-suite')
| -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/); +}); |
