返回 DeepSeek-Reasonix
browser-control-settings.test.tsx
根目录 / desktop / frontend / src / __tests__ / browser-control-settings.test.tsx
1 import { JSDOM } from "jsdom";
2 import React from "react";
3 import { act } from "react";
4 import { createRoot } from "react-dom/client";
5 import { BrowserControlSettingsPage } from "../components/BrowserControlSettingsPage";
6 import { LocaleProvider } from "../lib/i18n";
7 import { installDesktopHostStub, type DesktopHostStubOptions } from "./desktopHostStub";
8
9 function ok(value: unknown, message: string) {
10 if (!value) throw new Error(message);
11 }
12
13 function flush(): Promise<void> {
14 return new Promise((resolve) => setTimeout(resolve, 0));
15 }
16
17 async function waitFor(label: string, predicate: () => boolean) {
18 for (let attempt = 0; attempt < 40; attempt += 1) {
19 await act(async () => {
20 await flush();
21 });
22 if (predicate()) return;
23 }
24 throw new Error(`timed out waiting for ${label}: ${document.body?.textContent?.slice(0, 400) ?? ""}`);
25 }
26
27 function installDom() {
28 const dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', {
29 pretendToBeVisual: true,
30 url: "http://localhost/",
31 });
32 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
33 globalThis.window = dom.window as unknown as Window & typeof globalThis;
34 globalThis.document = dom.window.document;
35 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
36 globalThis.Node = dom.window.Node;
37 globalThis.HTMLElement = dom.window.HTMLElement;
38 globalThis.HTMLButtonElement = dom.window.HTMLButtonElement;
39 globalThis.HTMLInputElement = dom.window.HTMLInputElement;
40 globalThis.Event = dom.window.Event;
41 globalThis.MouseEvent = dom.window.MouseEvent;
42 globalThis.localStorage = dom.window.localStorage;
43 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
44 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
45 return dom;
46 }
47
48 async function renderPage(options: DesktopHostStubOptions = {}) {
49 const stub = installDesktopHostStub({}, options);
50 const rootEl = document.getElementById("root");
51 if (!rootEl) throw new Error("missing root");
52 const root = createRoot(rootEl);
53 await act(async () => {
54 root.render(React.createElement(LocaleProvider, null, React.createElement(BrowserControlSettingsPage)));
55 await flush();
56 });
57 return { stub, root, rootEl };
58 }
59
60 function switchFor(rootEl: HTMLElement, label: string): HTMLInputElement {
61 const found = Array.from(rootEl.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')).find(
62 (input) => input.getAttribute("aria-label") === label,
63 );
64 ok(found, `missing switch for ${label}`);
65 return found as HTMLInputElement;
66 }
67
68 function buttonFor(rootEl: HTMLElement, text: string): HTMLButtonElement {
69 const found = Array.from(rootEl.querySelectorAll("button")).find((button) => (button.textContent || "").includes(text));
70 ok(found, `missing button ${text}: ${rootEl.textContent ?? ""}`);
71 return found as HTMLButtonElement;
72 }
73
74 async function click(target: HTMLElement) {
75 await act(async () => {
76 target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
77 await flush();
78 });
79 }
80
81 console.log("browser control settings page");
82
83 {
84 const calls: string[] = [];
85 installDom();
86 // English labels keep the button-text assertions stable.
87 window.localStorage.setItem("reasonix-lang", "en");
88 const { root, rootEl } = await renderPage({ browserControlCalls: calls });
89 await waitFor("controls", () => rootEl.querySelectorAll('input[type="checkbox"]').length === 2);
90
91 ok((rootEl.textContent || "").includes("Enable built-in browser control"), "control row must render");
92 ok((rootEl.textContent || "").includes("Clear all browser data"), "data rows must render");
93
94 const control = switchFor(rootEl, "Enable built-in browser control");
95 ok(control.checked, "control starts enabled");
96 await click(control);
97 await waitFor("control toggle", () => calls.includes("setEnabled:false"));
98 ok((rootEl.textContent || "").includes("Built-in browser control disabled"), "toggle must confirm with a notice");
99 ok(!switchFor(rootEl, "Enable built-in browser control").checked, "switch must reflect the new state");
100
101 const certificates = switchFor(rootEl, "Ignore certificate errors");
102 ok(!certificates.checked, "certificate verification starts strict");
103 await click(certificates);
104 await waitFor("certificate toggle", () => calls.includes("setIgnoreCertificateErrors:true"));
105
106 await click(buttonFor(rootEl, "Clear cache"));
107 await waitFor("cache clear", () => calls.includes("clearCache"));
108 ok((rootEl.textContent || "").includes("Built-in browser cache cleared"), "cache clear must confirm");
109
110 await click(buttonFor(rootEl, "Import browser data"));
111 await waitFor("import", () => calls.includes("importChromeLogin"));
112 ok((rootEl.textContent || "").includes("Imported 12 cookies from Default"), "import must report what it copied");
113
114 // Clearing everything is destructive, so the first click only arms the confirm.
115 await click(buttonFor(rootEl, "Clear all"));
116 ok(!calls.includes("clearAllData"), "first click must not clear anything");
117 await click(buttonFor(rootEl, "Confirm clear"));
118 await waitFor("clear all", () => calls.includes("clearAllData"));
119 ok((rootEl.textContent || "").includes("Built-in browser data cleared"), "clear all must confirm");
120
121 await act(async () => {
122 root.unmount();
123 });
124 ok(calls.includes("setEnabled:false"), `recorded calls: ${calls.join(",")}`);
125 }
126
127 {
128 installDom();
129 window.localStorage.setItem("reasonix-lang", "en");
130 const { root, rootEl } = await renderPage({ chromeImportOutcome: { ok: false, reason: "safe-storage-denied" } });
131 await waitFor("import button", () => rootEl.querySelectorAll("button").length > 0);
132 await click(buttonFor(rootEl, "Import browser data"));
133 await waitFor("import error", () => (rootEl.textContent || "").includes("Chrome Safe Storage"));
134 ok(rootEl.querySelector('[role="alert"]'), "a denied keychain prompt must surface as an alert");
135 await act(async () => {
136 root.unmount();
137 });
138 }
139
140 {
141 installDom();
142 window.localStorage.setItem("reasonix-lang", "en");
143 const rootEl = document.getElementById("root");
144 if (!rootEl) throw new Error("missing root");
145 const root = createRoot(rootEl);
146 await act(async () => {
147 root.render(React.createElement(LocaleProvider, null, React.createElement(BrowserControlSettingsPage)));
148 await flush();
149 });
150 // No Electron preload is installed here, which is the browser/Serve shell.
151 await waitFor("desktop-only notice", () => (rootEl.textContent || "").includes("desktop app"));
152 ok(rootEl.querySelectorAll('input[type="checkbox"]').length === 0, "no browser control must render without the shell");
153 await act(async () => {
154 root.unmount();
155 });
156 }
157
157 lines Plain Text