aboutsummaryrefslogtreecommitdiff
path: root/test-suite/unit/overlay.test.mjs
blob: b533ff8390ec3af18b0f5b43819d9aba8756c885 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 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", "<com-1password-uso> 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/);
});