返回 DeepSeek-Reasonix
search-footnotes-render.test.tsx
根目录 / desktop / frontend / src / __tests__ / search-footnotes-render.test.tsx
1 // Run: tsx src/__tests__/search-footnotes-render.test.tsx
2
3 import { JSDOM } from "jsdom";
4 import { registerHooks } from "node:module";
5 import React, { act } from "react";
6 import { createRoot } from "react-dom/client";
7 import { LocaleProvider } from "../lib/i18n";
8 import { AssistantMessage } from "../components/Message";
9 import { hydrateReasoningDisplayMode } from "../lib/reasoningDisplayPreference";
10
11 registerHooks({
12 resolve(specifier, context, nextResolve) {
13 if (specifier.endsWith(".css")) {
14 return nextResolve("./asset-stub-for-tests.ts", { ...context, parentURL: import.meta.url });
15 }
16 return nextResolve(specifier, context);
17 },
18 });
19
20 let passed = 0;
21 let failed = 0;
22
23 function ok(value: boolean, label: string) {
24 if (value) {
25 process.stdout.write(` PASS ${label}\n`);
26 passed += 1;
27 } else {
28 process.stdout.write(` FAIL ${label}\n`);
29 failed += 1;
30 }
31 }
32
33 console.log("\nsearch sources panel render");
34
35 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
36 pretendToBeVisual: true,
37 url: "http://localhost/",
38 });
39 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
40 globalThis.window = dom.window as unknown as Window & typeof globalThis;
41 globalThis.document = dom.window.document;
42 Object.defineProperty(globalThis, "navigator", { configurable: true, value: { ...dom.window.navigator, language: "en-US" } });
43 globalThis.Node = dom.window.Node;
44 globalThis.Element = dom.window.Element;
45 globalThis.HTMLElement = dom.window.HTMLElement;
46 globalThis.Event = dom.window.Event;
47 globalThis.CustomEvent = dom.window.CustomEvent;
48 globalThis.localStorage = dom.window.localStorage;
49 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
50 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
51
52 const rootEl = document.getElementById("root");
53 if (!rootEl) throw new Error("missing root");
54 const root = createRoot(rootEl);
55 hydrateReasoningDisplayMode("hidden", true);
56
57 await act(async () => {
58 root.render(
59 <LocaleProvider>
60 <AssistantMessage
61 item={{
62 kind: "assistant",
63 id: "a1",
64 text: "answer only",
65 reasoning: "",
66 streaming: false,
67 searchSources: [{ title: "新闻本文", url: "https://example.com/a?utm_source=test#section" }],
68 }}
69 />
70 </LocaleProvider>,
71 );
72 });
73
74 const body = document.querySelector(".msg__body");
75 const panel = document.querySelector(".msg-search-sources");
76 const toggle = panel?.querySelector<HTMLButtonElement>(".msg-search-sources__toggle");
77 ok(Boolean(body?.textContent?.includes("answer only")), "answer body keeps the model text");
78 ok(Boolean(panel), "sources panel renders under the answer");
79 ok(toggle?.getAttribute("aria-expanded") === "false", "sources panel is collapsed by default");
80 ok(!panel?.querySelector(".msg-search-sources__body"), "collapsed panel does not expose source details");
81 await act(async () => {
82 toggle?.click();
83 });
84 const title = panel?.querySelector(".msg-search-source__title");
85 const link = panel?.querySelector<HTMLAnchorElement>(".msg-search-source__link");
86 ok(toggle?.getAttribute("aria-expanded") === "true", "click expands the sources panel");
87 ok(title?.textContent === "新闻本文", "expanded panel shows the result title");
88 ok(link?.getAttribute("href") === "https://example.com/a", "source link uses the cleaned URL");
89 ok(Boolean(panel?.querySelector(".msg-search-source__url")?.textContent?.includes("example.com/a")), "expanded panel shows the hostname and short URL");
90 ok(!(document.querySelector(".msg__body > .md")?.textContent ?? "").includes("新闻本文"), "answer markdown does not include the search title");
91
92 if (failed) {
93 process.stdout.write(`\n${failed} failed, ${passed} passed\n`);
94 process.exit(1);
95 }
96 process.stdout.write(`\n${passed} passed\n`);
97
97 lines Plain Text