anvilsign in

collin/browser-terminal-extension

main / extension / lib / split.test.js
1// Run with: node --test "extension/lib/*.test.js"
2//
3// Chrome's split view is two visible tabs and one `active` flag, so the picker
4// runs in both halves at once. What is checked here is the race that follows:
5// who wins, who gets torn down, and which failures are allowed to end it.
6
7const test = require("node:test");
8const assert = require("node:assert");
9
10const Split = require("./split.js");
11
12/** Tabs whose injected picker is still open, keyed by tab id. */
13const pending = new Map();
14
15/**
16 * A stand-in for the scripting API. `pick` decides what each tab's injected
17 * picker does: a value resolves the pick, null cancels it, an Error rejects the
18 * injection, and a promise is awaited so a test can control who wins the race.
19 */
20function fakeApi(pick, log = []) {
21 return {
22 api: {
23 scripting: {
24 executeScript({ target, func }) {
25 // The cancel injection is a different function than the picker; it is
26 // the only one that takes no part in the race.
27 if (func.name === "tbCancelPicker") {
28 log.push(`cancel:${target.tabId}`);
29 const cancelled = pending.get(target.tabId);
30 if (cancelled) cancelled(null);
31 return Promise.resolve([{ result: undefined }]);
32 }
33 log.push(`pick:${target.tabId}`);
34 const outcome = pick(target.tabId);
35 if (outcome instanceof Error) return Promise.reject(outcome);
36 if (outcome === "wait") {
37 return new Promise((resolve) => {
38 pending.set(target.tabId, (v) => resolve([{ result: v }]));
39 });
40 }
41 return Promise.resolve([{ result: outcome }]);
42 },
43 },
44 tabs: {
45 query: () => Promise.resolve([]),
46 },
47 },
48 log,
49 };
50}
51
52test("a pick in the non-active half of a split wins the race", async () => {
53 pending.clear();
54 const { api, log } = fakeApi((id) => (id === 2 ? { xpath: "/html/body" } : "wait"));
55 const out = await Split.racePick(api, [{ id: 1 }, { id: 2 }], function tbPickElement() {});
56 assert.equal(out.tabId, 2);
57 assert.deepEqual(out.value, { xpath: "/html/body" });
58 assert.equal(out.error, "");
59 // The loser's overlay is torn down rather than left live in the other pane.
60 assert.ok(log.includes("cancel:1"));
61});
62
63test("an injection that fails does not end a pick the other half is serving", async () => {
64 pending.clear();
65 const { api } = fakeApi((id) =>
66 id === 1 ? new Error("Cannot access contents of the page") : { xpath: "/html" },
67 );
68 const out = await Split.racePick(api, [{ id: 1 }, { id: 2 }], function tbPickElement() {});
69 assert.equal(out.tabId, 2);
70 assert.deepEqual(out.value, { xpath: "/html" });
71});
72
73test("every injection failing reports the first reason against the active tab", async () => {
74 pending.clear();
75 const { api } = fakeApi(() => new Error("Cannot access contents of the page"));
76 const out = await Split.racePick(api, [{ id: 1 }, { id: 2 }], function tbPickElement() {});
77 assert.equal(out.tabId, 1);
78 assert.equal(out.value, null);
79 assert.match(out.error, /Cannot access/);
80});
81
82test("a cancel in one half cancels the other", async () => {
83 pending.clear();
84 const { api, log } = fakeApi((id) => (id === 1 ? null : "wait"));
85 const out = await Split.racePick(api, [{ id: 1 }, { id: 2 }], function tbPickElement() {});
86 assert.equal(out.value, null);
87 assert.equal(out.error, "");
88 assert.ok(log.includes("cancel:2"));
89});
90
91test("no pickable tab is a message, not a throw", async () => {
92 const { api } = fakeApi(() => null);
93 const out = await Split.racePick(api, [], function tbPickElement() {});
94 assert.equal(out.value, null);
95 assert.match(out.error, /no page/);
96});
97
98test("a tab outside a split is the only target", async () => {
99 const { api } = fakeApi(() => null);
100 const targets = await Split.pickTargets(api, { id: 7, url: "https://example.com/" });
101 assert.deepEqual(targets, [{ id: 7, url: "https://example.com/" }]);
102});
103
104test("a split's other half comes back too, with the active tab first", async () => {
105 const api = {
106 scripting: { executeScript: () => Promise.resolve([]) },
107 tabs: { query: () => Promise.resolve([{ id: 3 }, { id: 9 }]) },
108 };
109 const targets = await Split.pickTargets(api, { id: 9, splitViewId: 4 });
110 assert.deepEqual(
111 targets.map((t) => t.id),
112 [9, 3],
113 );
114});
115
116test("a browser that rejects the splitViewId query falls back to the active tab", async () => {
117 const api = {
118 scripting: { executeScript: () => Promise.resolve([]) },
119 tabs: {
120 query: () => Promise.reject(new Error("Unexpected property: 'splitViewId'")),
121 },
122 };
123 const targets = await Split.pickTargets(api, { id: 9, splitViewId: 4 });
124 assert.deepEqual(
125 targets.map((t) => t.id),
126 [9],
127 );
128});