返回 DeepSeek-Reasonix
surfaceManager.test.ts
根目录 / desktop / electron / src / main / browser / surfaceManager.test.ts
1 import assert from "node:assert/strict";
2 import { test } from "node:test";
3 import type { BrowserTabView } from "../../shared/ipc.js";
4 import { FakeGuestView, FakeViewFactory, silentLog } from "./fakeGuestViews.js";
5 import { BrowserSurfaceManager, normaliseBrowserURL, validateLayout, type BrowserTab } from "./surfaceManager.js";
6
7 function setup(options: { now?: () => number } = {}) {
8 const factory = new FakeViewFactory();
9 const takeovers: Array<{ tabId: string; epoch: number; reason: string }> = [];
10 const crashes: string[] = [];
11 const broadcasts: BrowserTabView[][] = [];
12 const manager = new BrowserSurfaceManager({
13 views: factory,
14 contentSize: () => ({ width: 1000, height: 700 }),
15 onTakeover: (tab: BrowserTab, reason: string) => takeovers.push({ tabId: tab.id, epoch: tab.epoch, reason }),
16 onCrash: (_tab, reason) => crashes.push(reason),
17 log: silentLog,
18 now: options.now,
19 openWaitMs: 10,
20 });
21 manager.subscribe((tabs) => broadcasts.push(tabs));
22 const viewOf = (tab: BrowserTab) => tab.view as FakeGuestView;
23 return { factory, manager, takeovers, crashes, broadcasts, viewOf };
24 }
25
26 test("URLs are normalised to http(s) and bare hosts get https", () => {
27 assert.equal(normaliseBrowserURL("example.com"), "https://example.com/");
28 assert.equal(normaliseBrowserURL(" http://a.b/c?d=1 "), "http://a.b/c?d=1");
29 assert.throws(() => normaliseBrowserURL("file:///etc/passwd"), /only http\(s\)/);
30 assert.throws(() => normaliseBrowserURL("javascript:alert(1)"), /only http\(s\)/);
31 assert.throws(() => normaliseBrowserURL(""), /empty/);
32 });
33
34 test("layout rects are rounded, clamped to the window and never negative", () => {
35 assert.deepEqual(validateLayout({ x: 10.4, y: -5, width: 2000, height: 300.6 }, { width: 1000, height: 700 }), { x: 10, y: 0, width: 990, height: 301 });
36 assert.deepEqual(validateLayout({ x: 1200, y: 0, width: 100, height: 100 }, { width: 1000, height: 700 }), { x: 1200, y: 0, width: 0, height: 100 });
37 assert.throws(() => validateLayout({ x: Number.NaN, y: 0, width: 1, height: 1 }, null), /finite/);
38 });
39
40 test("open, activate, layout and overlay drive visibility of exactly one view", async () => {
41 const { manager, viewOf } = setup();
42 const first = await manager.open("example.com", { taskId: "task-1", temporary: false });
43 const second = await manager.open("https://b.test", { taskId: "task-1", temporary: true });
44 assert.equal(first.partition, "persist:browser");
45 assert.equal(second.partition, `temp:${second.id}`);
46 assert.equal(viewOf(first).page.calls[0], "load:https://example.com/");
47 assert.equal(manager.activeTabId, null, "opens do not select a native view without the application renderer");
48 manager.activate(second.id);
49 assert.equal(viewOf(second).visible, false, "no layout yet: nothing is visible");
50
51 manager.setLayout({ x: 300, y: 100, width: 600, height: 500 });
52 assert.equal(viewOf(second).visible, true);
53 assert.deepEqual(viewOf(second).bounds, { x: 300, y: 100, width: 600, height: 500 });
54 assert.equal(viewOf(first).visible, false);
55
56 manager.activate(first.id);
57 assert.equal(viewOf(first).visible, true);
58 assert.equal(viewOf(second).visible, false);
59
60 manager.setOverlay(true);
61 assert.equal(viewOf(first).visible, false);
62 manager.setOverlay(false);
63 assert.equal(viewOf(first).visible, true);
64
65 manager.setLayout(null);
66 assert.equal(viewOf(first).visible, false);
67 manager.setLayout({ x: 0, y: 0, width: 100, height: 100 });
68 manager.activate(null);
69 assert.equal(viewOf(first).visible, false);
70 assert.throws(() => manager.activate("tab-99"), /unknown browser tab/);
71 });
72
73 test("navigation bumps the epoch, records the URL and clears load errors", async () => {
74 const { manager, viewOf, broadcasts } = setup();
75 const tab = await manager.open("https://a.test", { taskId: "t", temporary: false });
76 manager.activate(tab.id);
77 const events = viewOf(tab).fire();
78 const before = broadcasts.length;
79 events.onStartLoading();
80 assert.equal(tab.loading, true);
81 events.onFailLoad(-105, "ERR_NAME_NOT_RESOLVED", "https://a.test/");
82 assert.deepEqual(tab.error, { code: -105, description: "ERR_NAME_NOT_RESOLVED" });
83 const epoch = tab.epoch;
84 events.onNavigate("https://a.test/next", false);
85 assert.equal(tab.epoch, epoch + 1);
86 assert.equal(tab.lastURL, "https://a.test/next");
87 assert.equal(tab.error, null);
88 events.onNavigate("https://a.test/next#x", true);
89 assert.equal(tab.epoch, epoch + 2);
90 events.onStopLoading();
91 assert.equal(tab.loading, false);
92 assert.ok(broadcasts.length >= before + 5, "every change is broadcast");
93 const view = manager.list()[0];
94 assert.equal(view.mode, "agent");
95 assert.equal(view.taskId, "t");
96 assert.equal(view.active, true);
97 });
98
99 test("take-over flips to human mode, bumps the epoch and reports to Go; resume hands back", async () => {
100 let now = 1000;
101 const { manager, takeovers } = setup({ now: () => now });
102 const tab = await manager.open("https://a.test", { taskId: "t", temporary: false });
103 const epoch = tab.epoch;
104 assert.equal(manager.takeoverFromSender(999, "mousedown"), false, "unknown senders are ignored");
105 assert.equal(manager.takeoverFromSender(tab.view.page.id, "mousedown"), true);
106 assert.equal(tab.mode, "human");
107 assert.equal(tab.epoch, epoch + 1);
108 assert.deepEqual(takeovers, [{ tabId: tab.id, epoch: epoch + 1, reason: "user mousedown" }]);
109
110 manager.resume(tab.id);
111 assert.equal(tab.mode, "agent");
112 assert.equal(tab.epoch, epoch + 2);
113 manager.resume(tab.id);
114 assert.equal(tab.epoch, epoch + 2, "resume is idempotent");
115
116 manager.markAgentInput(tab);
117 now += 100;
118 assert.equal(manager.takeoverFromSender(tab.view.page.id, "keydown"), false, "the agent's own echoed input is not a take-over");
119 assert.equal(tab.mode, "agent");
120 now += 1000;
121 assert.equal(manager.takeoverFromSender(tab.view.page.id, "keydown"), true);
122 assert.equal(tab.mode, "human");
123 });
124
125 test("a crashed website view reloads its last URL in human mode and emits crash", async () => {
126 const { manager, viewOf, crashes } = setup();
127 const tab = await manager.open("https://a.test", { taskId: "t", temporary: false });
128 const events = viewOf(tab).fire();
129 events.onNavigate("https://a.test/page", false);
130 const epoch = tab.epoch;
131 const partition = tab.partition;
132 events.onRenderProcessGone("crashed");
133 assert.equal(tab.mode, "human");
134 assert.equal(tab.epoch, epoch + 1);
135 // Promoted from prototypes/electron-browser: the reload keeps the persistent
136 // partition, so website logins survive a renderer crash.
137 assert.equal(tab.partition, partition, "recovery preserves the login partition");
138 assert.deepEqual(crashes, ["crashed"]);
139 assert.equal(viewOf(tab).page.calls.at(-1), "load:https://a.test/page");
140 for (let i = 0; i < 5; i++) events.onRenderProcessGone("oom");
141 assert.equal(viewOf(tab).page.calls.filter((call) => call === "load:https://a.test/page").length, 3, "reloads are capped");
142 });
143
144 test("popups retain task and partition without stealing the application selection", async () => {
145 const { manager, viewOf, factory } = setup();
146 const tab = await manager.open("https://a.test", { taskId: "t", temporary: true });
147 manager.activate(tab.id);
148 const adopt = viewOf(tab).fire().onPopup("https://login.test/oauth", "new-window");
149 assert.ok(adopt);
150 const child = factory.create(tab.partition) as FakeGuestView;
151 adopt(child);
152 const tabs = manager.all();
153 assert.equal(tabs.length, 2);
154 assert.equal(tabs[1].taskId, "t");
155 assert.equal(tabs[1].partition, tab.partition);
156 assert.equal(tabs[1].temporary, true);
157 assert.equal(manager.activeTabId, tab.id);
158 assert.ok(child.events, "the popup view is bound to its own tab record");
159 manager.close(tab.id);
160 assert.equal(viewOf(tab).fire().onPopup("https://x", "new-window"), null, "a closed tab cannot spawn popups");
161 });
162
163 test("close and destroyAll tear views down and leave replacement selection to the renderer", async () => {
164 const { manager, viewOf } = setup();
165 const a = await manager.open("https://a.test", { taskId: "t", temporary: false });
166 const b = await manager.open("https://b.test", { taskId: "t", temporary: false });
167 manager.activate(b.id);
168 manager.setLayout({ x: 0, y: 0, width: 10, height: 10 });
169 manager.close(b.id);
170 assert.equal(viewOf(b).destroyed, true);
171 assert.equal(manager.activeTabId, null);
172 assert.equal(viewOf(a).visible, false);
173 manager.activate(a.id);
174 assert.equal(viewOf(a).visible, true);
175 assert.equal(b.mode, "human", "a closed tab fails every pending act as taken over");
176 viewOf(a).fire().onDestroyed();
177 assert.equal(manager.all().length, 0, "a page closing itself drops the record");
178 const c = await manager.open("https://c.test", { taskId: "t", temporary: false });
179 manager.destroyAll();
180 assert.equal(viewOf(c).destroyed, true);
181 assert.equal(manager.all().length, 0);
182 assert.equal(c.mode, "human");
183 });
184
185 test("background opens never replace the current task's page or revive a detached renderer", async () => {
186 const { manager, viewOf, takeovers } = setup();
187 const a = await manager.open("https://a.test", { taskId: "A", temporary: false });
188 manager.activate(a.id);
189 manager.setLayout({ x: 400, y: 100, width: 500, height: 500 });
190 const b = await manager.open("https://b.test", { taskId: "B", temporary: false });
191 assert.equal(manager.activeTabId, a.id);
192 assert.equal(viewOf(a).visible, true);
193 assert.equal(viewOf(b).visible, false);
194 const epoch = a.epoch;
195 manager.pauseForRendererLoss("app renderer crashed");
196 assert.equal(manager.activeTabId, null);
197 assert.equal(viewOf(a).visible, false);
198 assert.equal(a.mode, "human");
199 assert.ok(a.epoch > epoch, "pending acts lose their document epoch");
200 assert.equal(takeovers.length, 2);
201 manager.activate(a.id);
202 assert.equal(viewOf(a).visible, false, "selection cannot restore the lost layout");
203 manager.setLayout({ x: 400, y: 100, width: 500, height: 500 });
204 assert.equal(viewOf(a).visible, true);
205 assert.equal(a.mode, "human", "remount never resumes actions automatically");
206 });
207
208 test("navigate, zoom and devtools act on the page", async () => {
209 const { manager, viewOf } = setup();
210 const tab = await manager.open("https://a.test", { taskId: "t", temporary: false });
211 const page = viewOf(tab).page;
212 page.back = true;
213 await manager.navigate(tab.id, { action: "back" });
214 await manager.navigate(tab.id, { action: "forward" });
215 await manager.navigate(tab.id, { action: "reload" });
216 await manager.navigate(tab.id, { action: "stop" });
217 await manager.navigate(tab.id, { url: "b.test/x" });
218 assert.deepEqual(page.calls.slice(1), ["back", "reload", "stop", "load:https://b.test/x"]);
219 await assert.rejects(manager.navigate(tab.id, {}), /needs a url/);
220 manager.setZoom(tab.id, 9);
221 assert.equal(tab.zoom, 5);
222 assert.equal(page.zoom, 5);
223 assert.throws(() => manager.setZoom(tab.id, Number.NaN), /finite/);
224 manager.toggleDevTools(tab.id);
225 assert.equal(page.devtools, true);
226 manager.toggleDevTools(tab.id);
227 assert.equal(page.devtools, false);
228 });
229
229 lines TYPESCRIPT