aboutsummaryrefslogtreecommitdiff
path: root/test-suite
diff options
context:
space:
mode:
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/);
+});