返回 DeepSeek-Reasonix
markdown-local-file-pending.test.tsx
根目录 / desktop / frontend / src / __tests__ / markdown-local-file-pending.test.tsx
1 import assert from "node:assert/strict";
2 import { JSDOM } from "jsdom";
3 import React, { Fragment, act } from "react";
4 import { createRoot } from "react-dom/client";
5 import type { AppBindings } from "../lib/bridge";
6 import { installDesktopHostStub } from "./desktopHostStub";
7 import { parseMarkdownToBlocks } from "../lib/markdownPipeline";
8 import { hastBlockToJsx } from "../lib/hastJsx";
9 import { createComponents } from "../components/markdownComponents";
10 import { LocaleProvider } from "../lib/i18n";
11 import { useBrowserPanelStore } from "../lib/browserPanelStore";
12
13 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", { url: "http://localhost/" });
14 Object.assign(globalThis, {
15 window: dom.window, document: dom.window.document, Node: dom.window.Node,
16 HTMLElement: dom.window.HTMLElement, MouseEvent: dom.window.MouseEvent,
17 Event: dom.window.Event, IS_REACT_ACT_ENVIRONMENT: true,
18 });
19
20 let resolveReferences!: (value: {
21 turnKey: string;
22 references: Array<{ key: string; path: string; status: "resolved"; displayPath: string; kind: string; actions: string[] }>;
23 }) => void;
24 let pendingCandidates: Array<{ key: string; path: string }> = [];
25 let nativeOpens = 0;
26 const previewCalls: string[] = [];
27 const browserTab = {
28 id: "html-tab", taskId: "tab-a", url: "http://127.0.0.1/preview", title: "index.html",
29 loading: false, canGoBack: false, canGoForward: false, temporary: false,
30 mode: "agent" as const, epoch: 0, zoom: 1, error: null,
31 };
32
33 const stub = installDesktopHostStub(({
34 main: { App: {
35 ResolveChatFileReferencesForTab: async (_tabId: string, turnKey: string, candidates: Array<{ key: string; path: string }>) => {
36 pendingCandidates = candidates;
37 return new Promise((resolve) => { resolveReferences = resolve; });
38 },
39 OpenFileBrowserPreviewForTab: async (_tabId: string, request: { path: string }) => {
40 previewCalls.push(request.path);
41 return { tabId: browserTab.id, url: browserTab.url, status: "opened", sessionGeneration: 1 };
42 },
43 OpenLocalPath: async () => { nativeOpens += 1; },
44 } as Partial<AppBindings> as AppBindings },
45 }).main.App);
46
47 useBrowserPanelStore.setState({
48 host: {
49 list: async () => [browserTab], activate: async () => {}, close: async () => {},
50 } as unknown as NonNullable<ReturnType<typeof useBrowserPanelStore.getState>["host"]>,
51 taskId: "tab-a", tabs: [], shown: [], visible: true,
52 });
53
54 const blocks = parseMarkdownToBlocks("[Open the page](file:///repo/out/index.html)");
55 const components = createComponents(false);
56 const { ChatFileScopeProvider, ChatFileTurnProvider, useChatFileCandidateReport } = await import("../components/ChatFileLinkContext");
57 function Reporter() {
58 useChatFileCandidateReport(blocks, 1);
59 return null;
60 }
61
62 const rootElement = document.getElementById("root")!;
63 const root = createRoot(rootElement);
64 await act(async () => {
65 root.render(<LocaleProvider>
66 <ChatFileScopeProvider scopeKey="session-a" tabId="tab-a">
67 <ChatFileTurnProvider turnKey="turn-a" factsVersion={1} presentedFiles={[]} modifiedFiles={[]} tabId="tab-a">
68 <Reporter />
69 {blocks.map((block) => <Fragment key={block.key}>{hastBlockToJsx(block, components)}</Fragment>)}
70 </ChatFileTurnProvider>
71 </ChatFileScopeProvider>
72 </LocaleProvider>);
73 });
74 await act(async () => { await Promise.resolve(); await Promise.resolve(); });
75
76 const pendingLink = rootElement.querySelector<HTMLAnchorElement>("a");
77 assert.ok(pendingLink, "the explicit local file remains a pending in-app link");
78 await act(async () => {
79 pendingLink.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true, cancelable: true }));
80 });
81 assert.equal(nativeOpens, 0, "the first click never falls through to the OS opener");
82 assert.equal(previewCalls.length, 0, "opening waits for host verification");
83
84 await act(async () => {
85 resolveReferences({
86 turnKey: "turn-a",
87 references: pendingCandidates.map((candidate) => ({
88 key: candidate.key, path: candidate.path, status: "resolved", displayPath: "out/index.html",
89 kind: "html", actions: ["preview", "source", "browser", "reveal-tree", "copy-path", "save-copy"],
90 })),
91 });
92 await Promise.resolve(); await Promise.resolve(); await Promise.resolve();
93 });
94
95 assert.deepEqual(previewCalls, ["out/index.html"], "the queued first click opens the host-verified HTML in the built-in browser");
96 assert.equal(nativeOpens, 0);
97 assert.equal(useBrowserPanelStore.getState().activeTabId, browserTab.id);
98
99 await act(async () => { root.unmount(); });
100 stub.uninstall();
101 dom.window.close();
102 console.log("PASS unresolved local HTML clicks wait for verification and stay in-app");
103
103 lines Plain Text