aboutsummaryrefslogtreecommitdiff
path: root/test-suite/unit/user-window.test.mjs
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-14 09:15:15 +0000
committerIgor Soarez <igor@soarez.org>2026-11-14 09:15:34 +0000
commita76fbc3b9b82038ab44c04d6379cc2d45e5cecd8 (patch)
tree7cf7425cfbca05a99b42198f78ff3fcf1a6eb5d8 /test-suite/unit/user-window.test.mjs
parente18559a1240219b977ffc7da2076c09cd96d1dad (diff)
tabs: one window for every agent; inactive tabs take input; background off by defaultHEADmaster
Measured in Brave on 2026-11-14: an inactive tab takes trusted CDP clicks, key presses, text and mouse moves, and Page.captureScreenshot renders it live. Only mouseWheel hangs, waiting for a frame. The upstream guard that refused input to every hidden page was wrong for inactive tabs, and it is what made a window per session look necessary. - New sessions' tabs open inactive in the user's last focused window, in the session's tab group; a window is created only when none is open. - New groups start collapsed, unless their tab is the one showing, and expand when a tab in them is brought forward. Existing groups keep the user's setting. - Input is refused up front only in a minimized window (untested there). - chrome_scroll scrolls hidden pages by script instead of the wheel. - Focus emulation on attach, so blur-sensitive menus stay open. - Background mode is off by default. Calls still default to background; background:false or chrome_tab activate brings a tab forward. - Agent guidance, tool descriptions and docs say so.
Diffstat (limited to 'test-suite/unit/user-window.test.mjs')
-rw-r--r--test-suite/unit/user-window.test.mjs119
1 files changed, 119 insertions, 0 deletions
diff --git a/test-suite/unit/user-window.test.mjs b/test-suite/unit/user-window.test.mjs
new file mode 100644
index 0000000..af4e939
--- /dev/null
+++ b/test-suite/unit/user-window.test.mjs
@@ -0,0 +1,119 @@
+// Agents' tabs go in the user's window, inactive, in per-session tab groups that start collapsed.
+// An inactive tab takes trusted input and renders screenshots (measured 2026-11-14), so there is no
+// need for a window per session. A window is created only when Brave has none.
+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");
+
+function worker({ groups = [], windows = { 5: { id: 5, type: "normal" } }, lastFocused = 5, tabs = {} } = {}) {
+ const calls = [];
+ const listener = { addListener() {}, removeListener() {} };
+ let nextTab = 100;
+ const w = {
+ console, setTimeout, clearTimeout, setInterval: () => 0, navigator: { userAgent: "unit-test" }, fetch: async () => { throw new Error("offline"); },
+ chrome: {
+ runtime: { id: "test", 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([]) },
+ storage: { session: { get: async () => ({}), set: async (v) => { calls.push({ method: 'storage.set', json: JSON.stringify(v) }); } } },
+ tabGroups: { query: async () => groups, update: async (id, props) => { calls.push({ method: "tabGroups.update", id, props }); return { id, ...props }; } },
+ windows: {
+ get: async (id) => windows[id] ?? Promise.reject(new Error("No window")),
+ getLastFocused: async () => (lastFocused === null ? Promise.reject(new Error("No window")) : windows[lastFocused]),
+ getAll: async () => Object.values(windows),
+ update: async (id, props) => { calls.push({ method: "windows.update", id, props }); return { id }; },
+ create: async (props) => { calls.push({ method: "windows.create", props }); return { id: 9, tabs: [{ id: 900, windowId: 9 }] }; },
+ },
+ tabs: {
+ create: async (props) => { calls.push({ method: "tabs.create", props }); return { id: nextTab++, windowId: props.windowId }; },
+ query: async () => [], get: async (id) => tabs[id] ?? null, onUpdated: listener,
+ group: async () => 77,
+ update: async (id, props) => { calls.push({ method: "tabs.update", id, props }); return { id, ...props }; },
+ },
+ },
+ };
+ w.self = w;
+ vm.runInNewContext(workerSource, w);
+ return { w, calls };
+}
+
+const piGroup = (windowId, name = "Pi Session: claude-code 1 other") => ({ id: 1, windowId, title: name });
+const ops = (calls) => calls.filter((c) => c.method !== "storage.set").map((c) => c.method);
+const noWindows = { windows: {}, lastFocused: null };
+
+test("a new session's tab goes in the user's window, inactive", async () => {
+ const { w, calls } = worker({ groups: [piGroup(5)] });
+ await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine");
+ assert.deepEqual(ops(calls), ["tabs.create"]);
+ const props = calls.find((c) => c.method === "tabs.create").props;
+ assert.equal(props.windowId, 5);
+ assert.equal(props.active, false);
+});
+
+test("a window is created only when Brave has none", async () => {
+ const { w, calls } = worker(noWindows);
+ await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine");
+ assert.deepEqual(ops(calls), ["windows.create"]);
+ assert.equal(calls[0].props.focused, false);
+});
+
+test("a session that already has a group adds its tab to that group's window", async () => {
+ const { w, calls } = worker({ groups: [piGroup(7, "Pi Session: claude-code 2 mine")], windows: { 5: { id: 5 }, 7: { id: 7 } } });
+ await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine");
+ assert.deepEqual(ops(calls), ["tabs.create"]);
+ assert.equal(calls.find((c) => c.method === "tabs.create").props.windowId, 7);
+});
+
+test("chrome_tab new for a session with no group goes in the user's window, inactive", async () => {
+ const { w, calls } = worker({ groups: [piGroup(5)] });
+ w.groupTab = async (tab) => tab;
+ w.bringToFront = async () => {};
+ await w.dispatch("tab.new", { url: "https://x.test/", groupTitle: "Pi Session: claude-code 2 mine", sessionKey: "claude-code 2 mine" });
+ assert.deepEqual(ops(calls), ["tabs.create"]);
+ assert.equal(calls[0].props.windowId, 5);
+ assert.equal(calls[0].props.active, false);
+});
+
+test("chrome_tab new with no window open creates an unfocused one", async () => {
+ const { w, calls } = worker(noWindows);
+ w.groupTab = async (tab) => tab;
+ w.bringToFront = async () => {};
+ await w.dispatch("tab.new", { url: "https://x.test/", groupTitle: "Pi Session: claude-code 2 mine", sessionKey: "claude-code 2 mine" });
+ assert.deepEqual(ops(calls), ["windows.create"]);
+ assert.equal(calls[0].props.focused, false);
+});
+
+test("a new group starts collapsed; an existing group keeps the user's setting", async () => {
+ const { w, calls } = worker({ tabs: { 300: { id: 300, windowId: 5, active: false, groupId: -1 }, 301: { id: 301, windowId: 5, active: false, groupId: 77 } } });
+ w.groupRecord = async (id) => ({ id });
+ w.formatTab = async (t) => t;
+ await w.groupTab({ id: 300, windowId: 5, groupId: -1 }, "Pi Session: x", "blue");
+ await w.groupTab({ id: 301, windowId: 5, groupId: 77 }, "Pi Session: x", "blue");
+ const updates = calls.filter((c) => c.method === "tabGroups.update").map((c) => c.props.collapsed);
+ assert.deepEqual(updates, [true, undefined]);
+});
+
+test("a new group holding the tab that is showing is not collapsed", async () => {
+ const { w, calls } = worker({ tabs: { 300: { id: 300, windowId: 5, active: true, groupId: -1 } } });
+ w.groupRecord = async (id) => ({ id });
+ w.formatTab = async (t) => t;
+ await w.groupTab({ id: 300, windowId: 5, groupId: -1 }, "Pi Session: x", "blue");
+ assert.equal(calls.find((c) => c.method === "tabGroups.update").props.collapsed, false);
+});
+
+test("bringing a tab forward expands its group", async () => {
+ const { w, calls } = worker();
+ await w.bringToFront({ id: 300, windowId: 5, groupId: 77 }, { foreground: true, background: false });
+ assert.deepEqual(ops(calls), ["windows.update", "tabGroups.update", "tabs.update"]);
+ assert.equal(calls[1].props.collapsed, false);
+});
+
+test("input is sent to an inactive tab; only a minimized window refuses it", async () => {
+ const { w } = worker({ windows: { 5: { id: 5, state: "normal" }, 6: { id: 6, state: "minimized" } }, tabs: { 300: { id: 300, windowId: 5, active: false }, 301: { id: 301, windowId: 6, active: true } } });
+ w.pageVisibilityState = async () => "hidden";
+ await w.assertTabVisibleForInput({ id: 300, windowId: 5 }, {}, "chrome.click");
+ await assert.rejects(w.assertTabVisibleForInput({ id: 301, windowId: 6 }, {}, "chrome.click"), /minimized/);
+});