返回 DeepSeek-Reasonix
external-opener-preference-sync.test.tsx
根目录 / desktop / frontend / src / __tests__ / external-opener-preference-sync.test.tsx
1 // Run: tsx src/__tests__/external-opener-preference-sync.test.tsx
2
3 import { JSDOM } from "jsdom";
4 import React, { act } from "react";
5 import { createRoot } from "react-dom/client";
6
7 import { ExternalOpener, type ExternalOpenerBridge } from "../components/ExternalOpener";
8 import { LocaleProvider } from "../lib/i18n";
9 import { ToastProvider } from "../lib/toast";
10 import type { ExternalOpenersView } from "../lib/types";
11
12 let passed = 0;
13 let failed = 0;
14 function ok(value: boolean, label: string) {
15 process.stdout.write(` ${value ? "PASS" : "FAIL"} ${label}\n`);
16 if (value) passed += 1;
17 else failed += 1;
18 }
19 const flush = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
20 const dom = new JSDOM("<!doctype html><html><body></body></html>", {
21 pretendToBeVisual: true,
22 url: "http://localhost/",
23 });
24 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
25 globalThis.window = dom.window as unknown as Window & typeof globalThis;
26 globalThis.document = dom.window.document;
27 globalThis.Node = dom.window.Node;
28 globalThis.HTMLElement = dom.window.HTMLElement;
29 globalThis.Event = dom.window.Event;
30 globalThis.MouseEvent = dom.window.MouseEvent;
31 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
32 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
33
34 const openers = [
35 { id: "finder", name: "Finder", kind: "file-manager" as const },
36 { id: "xcode", name: "Xcode", kind: "editor" as const },
37 ];
38
39 function createHarness(bridge: ExternalOpenerBridge) {
40 const container = document.createElement("div");
41 document.body.append(container);
42 const root = createRoot(container);
43 const renderTab = async (tabId: string) => {
44 await act(async () => {
45 root.render(
46 <LocaleProvider>
47 <ToastProvider>
48 <ExternalOpener key={tabId} tabId={tabId} dismissSignal={0} bridge={bridge} />
49 </ToastProvider>
50 </LocaleProvider>,
51 );
52 await flush();
53 });
54 };
55 const choose = async (name: string) => {
56 await act(async () => {
57 container.querySelector<HTMLButtonElement>('button[aria-haspopup="menu"]')
58 ?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
59 await flush();
60 });
61 await act(async () => {
62 Array.from(container.querySelectorAll<HTMLButtonElement>('button[role="menuitemradio"]'))
63 .find((button) => button.textContent?.includes(name))
64 ?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
65 await flush();
66 });
67 };
68 const primaryLabel = () => container.querySelector<HTMLButtonElement>(".external-opener__primary")?.ariaLabel;
69 const unmount = async () => {
70 await act(async () => root.unmount());
71 container.remove();
72 };
73 return { container, renderTab, choose, primaryLabel, unmount };
74 }
75
76 console.log("\nexternal opener preference synchronization");
77
78 let preferred = "finder";
79 let resolveOlderLaunch: (() => void) | undefined;
80 const launches: string[] = [];
81 const completionBridge: ExternalOpenerBridge = {
82 async ExternalOpenersForTab() { return { openers, preferred, workspaceOpenable: true }; },
83 async SetPreferredExternalOpener(id) { preferred = id; },
84 async OpenWorkspaceInExternalOpenerForTab(tabId, id) {
85 launches.push(`${tabId}:${id}`);
86 if (tabId === "older") await new Promise<void>((resolve) => { resolveOlderLaunch = resolve; });
87 },
88 };
89 const completion = createHarness(completionBridge);
90 await completion.renderTab("older");
91 await completion.choose("Xcode");
92 await completion.renderTab("visible");
93 ok(completion.primaryLabel()?.includes("Finder") === true, "the visible tab initially discovers the old preference");
94 await act(async () => { resolveOlderLaunch?.(); await flush(); });
95 ok(preferred === "xcode" && completion.primaryLabel()?.includes("Xcode") === true, "an older tab completion synchronizes the visible control");
96 await act(async () => {
97 completion.container.querySelector<HTMLButtonElement>(".external-opener__primary")
98 ?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
99 await flush();
100 });
101 ok(launches.at(-1) === "visible:xcode", "the visible primary action uses the synchronized preference");
102 await completion.unmount();
103
104 preferred = "finder";
105 let resolveSecondOlderLaunch: (() => void) | undefined;
106 let resolveStaleDiscovery: ((view: ExternalOpenersView) => void) | undefined;
107 const staleDiscoveryBridge: ExternalOpenerBridge = {
108 async ExternalOpenersForTab(tabId) {
109 if (tabId !== "visible") return { openers, preferred, workspaceOpenable: true };
110 return new Promise<ExternalOpenersView>((resolve) => { resolveStaleDiscovery = resolve; });
111 },
112 async SetPreferredExternalOpener(id) { preferred = id; },
113 async OpenWorkspaceInExternalOpenerForTab(tabId) {
114 if (tabId === "older") await new Promise<void>((resolve) => { resolveSecondOlderLaunch = resolve; });
115 },
116 };
117 const staleDiscovery = createHarness(staleDiscoveryBridge);
118 await staleDiscovery.renderTab("older");
119 await staleDiscovery.choose("Xcode");
120 await staleDiscovery.renderTab("visible");
121 await act(async () => { resolveSecondOlderLaunch?.(); await flush(); });
122 await act(async () => {
123 resolveStaleDiscovery?.({ openers, preferred: "finder", workspaceOpenable: true });
124 await flush();
125 });
126 ok(staleDiscovery.primaryLabel()?.includes("Xcode") === true, "a discovery started before the write cannot restore the stale preference");
127 await staleDiscovery.unmount();
128
129 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
130 if (failed > 0) process.exit(1);
131
131 lines Plain Text