返回 DeepSeek-Reasonix
browser-surface-layout.test.ts
根目录 / desktop / frontend / src / __tests__ / browser-surface-layout.test.ts
1 // Run: tsx src/__tests__/browser-surface-layout.test.ts
2 import assert from "node:assert/strict";
3 import { JSDOM } from "jsdom";
4
5 import type { BrowserLayoutRect } from "../lib/browserHost";
6 import { attachAppOverlayGate, attachBrowserSurfaceLayout } from "../lib/useBrowserSurfaceLayout";
7
8 const dom = new JSDOM("<!doctype html><html><body><main id='scroller'><div id='surface'></div></main></body></html>", { url: "http://localhost" });
9 const view = dom.window as unknown as Window & typeof globalThis;
10 const frames: FrameRequestCallback[] = [];
11 let cancelled = 0;
12 view.requestAnimationFrame = (callback) => { frames.push(callback); return frames.length; };
13 view.cancelAnimationFrame = (id) => { cancelled += 1; frames[id - 1] = () => {}; };
14 const flush = () => { for (const callback of frames.splice(0)) callback(0); };
15
16 class FakeResizeObserver {
17 static instances: FakeResizeObserver[] = [];
18 observed: Element[] = [];
19 constructor(readonly callback: ResizeObserverCallback) { FakeResizeObserver.instances.push(this); }
20 observe(target: Element) { this.observed.push(target); }
21 unobserve() {}
22 disconnect() { this.observed = []; }
23 trigger() { this.callback([], this as unknown as ResizeObserver); }
24 }
25 (view as unknown as { ResizeObserver: unknown }).ResizeObserver = FakeResizeObserver;
26
27 const surface = view.document.getElementById("surface")!;
28 const scroller = view.document.getElementById("scroller")!;
29 let box = { left: 10.4, top: 20.6, width: 300.2, height: 199.5 };
30 surface.getBoundingClientRect = () => ({ ...box, right: box.left + box.width, bottom: box.top + box.height, x: box.left, y: box.top, toJSON: () => box }) as DOMRect;
31
32 const layouts: (BrowserLayoutRect | null)[] = [];
33 const overlays: boolean[] = [];
34 const host = { setLayout: (rect: BrowserLayoutRect | null) => { layouts.push(rect); }, setOverlay: (active: boolean) => { overlays.push(active); } };
35 const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
36
37 console.log("\nbrowser surface layout");
38
39 const detachLayout = attachBrowserSurfaceLayout(host, surface, view);
40 assert.deepEqual(layouts, [{ x: 10, y: 21, width: 300, height: 200 }], "the surface rect is reported immediately, rounded to integers");
41 const observer = FakeResizeObserver.instances[0];
42 assert.deepEqual(observer.observed, [surface], "the surface is observed for size changes");
43 observer.trigger();
44 observer.trigger();
45 assert.equal(frames.length, 1, "notifications coalesce into one animation frame");
46 flush();
47 assert.equal(layouts.length, 1, "an unchanged rect is not resent");
48 box = { ...box, top: 60 };
49 scroller.dispatchEvent(new view.Event("scroll"));
50 flush();
51 assert.deepEqual(layouts[layouts.length - 1], { x: 10, y: 60, width: 300, height: 200 }, "scrolling an ancestor re-measures");
52 box = { ...box, width: 420 };
53 view.dispatchEvent(new view.Event("resize"));
54 flush();
55 assert.deepEqual(layouts[layouts.length - 1], { x: 10, y: 60, width: 420, height: 200 }, "window resize re-measures");
56 box = { ...box, left: 5 };
57 observer.trigger();
58 detachLayout();
59 assert.equal(cancelled, 1, "a pending frame is cancelled on detach");
60 assert.equal(layouts[layouts.length - 1], null, "detach hides the native view");
61 assert.deepEqual(observer.observed, [], "detach disconnects the observer");
62 const count = layouts.length;
63 scroller.dispatchEvent(new view.Event("scroll"));
64 view.dispatchEvent(new view.Event("resize"));
65 flush();
66 assert.equal(layouts.length, count, "listeners are removed on detach");
67 console.log(" PASS rect math, dedupe, ancestor scroll, resize and detach");
68
69 const detachOverlay = attachAppOverlayGate(host, view);
70 assert.deepEqual(overlays, [false], "the initial overlay state is reported");
71 const modal = view.document.createElement("div");
72 modal.setAttribute("data-app-overlay", "");
73 view.document.body.appendChild(modal);
74 await settle();
75 flush();
76 assert.deepEqual(overlays, [false, true], "a portaled overlay root hides the native views");
77 const wrapper = view.document.createElement("div");
78 wrapper.innerHTML = "<section><div data-app-overlay=''></div></section>";
79 view.document.body.appendChild(wrapper);
80 await settle();
81 flush();
82 assert.deepEqual(overlays, [false, true], "a second overlay does not repeat the state");
83 modal.remove();
84 await settle();
85 flush();
86 assert.deepEqual(overlays, [false, true], "one remaining nested overlay keeps the state");
87 wrapper.remove();
88 await settle();
89 flush();
90 assert.deepEqual(overlays, [false, true, false], "removing the last overlay restores the views");
91 const plain = view.document.createElement("div");
92 view.document.body.appendChild(plain);
93 await settle();
94 flush();
95 assert.equal(overlays.length, 3, "unrelated mutations do not report");
96 plain.setAttribute("data-app-overlay", "");
97 await settle();
98 flush();
99 assert.deepEqual(overlays[overlays.length - 1], true, "toggling the attribute on an existing element counts");
100 detachOverlay();
101 assert.deepEqual(overlays[overlays.length - 1], false, "detach clears an active overlay state");
102 plain.remove();
103 await settle();
104 flush();
105 assert.equal(overlays.length, 5, "the observer is disconnected on detach");
106 console.log(" PASS overlay gate follows [data-app-overlay] presence");
107 console.log("browser surface layout: all checks passed");
108
108 lines TYPESCRIPT