返回 DeepSeek-Reasonix
markdown-natural-flow.test.tsx
根目录 / desktop / frontend / src / __tests__ / markdown-natural-flow.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { JSDOM } from "jsdom";
5 import { createServer } from "vite";
6 import { parseMarkdown } from "../lib/markdownPipeline";
7
8 const dom = new JSDOM('<div id="root"></div>', { url: "http://localhost", pretendToBeVisual: true });
9 Object.assign(globalThis, { window: dom.window, document: dom.window.document, HTMLElement: dom.window.HTMLElement,
10 Node: dom.window.Node, IS_REACT_ACT_ENVIRONMENT: true,
11 requestAnimationFrame: dom.window.requestAnimationFrame.bind(dom.window), cancelAnimationFrame: dom.window.cancelAnimationFrame.bind(dom.window) });
12 const server = await createServer({ server: { middlewareMode: true }, appType: "custom" });
13 const { default: History } = await server.ssrLoadModule("/src/components/MarkdownHistory.tsx");
14 const { LocaleProvider } = await server.ssrLoadModule("/src/lib/i18n.tsx");
15 const worker = await server.ssrLoadModule("/src/lib/markdownWorkerClient.ts");
16 const calls: Array<{ id: number; text: string }> = [];
17 const documents = new Map<string, string>();
18 const fake = { onmessage: null as null | ((event: { data: unknown }) => void), onerror: null,
19 postMessage(message: { id: number; op?: string; documentId?: string; text?: string }) {
20 if (message.op === "release") { documents.delete(message.documentId ?? ""); return; }
21 let text = message.text ?? "";
22 if (message.op === "append") text = (documents.get(message.documentId ?? "") ?? "") + text;
23 if (message.documentId) documents.set(message.documentId, text);
24 calls.push({ id: message.id, text });
25 }, terminate() {} };
26 Object.assign(globalThis, { Worker: class {} });
27 worker.setMarkdownWorkerClientForTest(new worker.MarkdownWorkerClient({ createWorker: async () => fake }));
28 const host = document.getElementById("root")!;
29 let root = createRoot(host);
30 async function render(text: string, streaming = false, onError?: () => void) {
31 await act(async () => root.render(<LocaleProvider><History text={text} streaming={streaming} cacheKey="natural-markdown" fallback={<div>{text}</div>} onError={onError} /></LocaleProvider>));
32 }
33 async function respond(index: number) {
34 const call = calls[index]; assert.ok(call);
35 await act(async () => fake.onmessage?.({ data: { id: call.id, result: parseMarkdown(call.text) } }));
36 }
37 try {
38 const prefix = "A **stable** paragraph.\n\n";
39 await render(prefix, true); await respond(0);
40 const paragraph = host.querySelector("p")!;
41 const selection = window.getSelection()!;
42 const range = document.createRange(); range.selectNodeContents(paragraph); selection.addRange(range);
43 const selected = selection.toString();
44 await render(prefix + "A changing tail", true); await respond(1);
45 assert.equal(host.querySelector("p"), paragraph, "stream prefix retains its native DOM host");
46 await render(prefix + "A changing tail\n\n[ref][id]\n\n[id]: https://example.com"); await respond(2);
47 assert.equal(host.querySelector("p"), paragraph, "settlement keeps the same paragraph");
48 assert.equal(selection.toString(), selected, "native selection survives settlement");
49 assert.equal(host.querySelector("a")?.getAttribute("href"), "https://example.com", "final parse resolves document references");
50 const final = calls[2].text;
51 await act(async () => root.unmount()); root = createRoot(host);
52 await render(final);
53 assert.equal(calls.length, 3, "cache avoids parsing unchanged history");
54 assert.ok(host.querySelector("strong"));
55 await render("obsolete source"); const old = calls.length - 1;
56 await render("replacement source");
57 await respond(old); // superseded active parse drains before the newest snapshot
58 await new Promise(resolve => setTimeout(resolve, 0));
59 const current = calls.length - 1;
60 await respond(current);
61 assert.equal(host.textContent, "replacement source", "superseded worker result cannot replace the current revision");
62 const large = Array.from({ length: 420 }, (_, index) => `Paragraph ${index}.`).join("\n\n");
63 await render(large); await new Promise(resolve => setTimeout(resolve, 0)); await respond(calls.length - 1);
64 assert.equal(host.querySelectorAll("p").length, 420, "all loaded Markdown blocks remain mounted");
65 assert.ok(host.textContent?.includes("Paragraph 0.") && host.textContent.includes("Paragraph 419."));
66 assert.equal(host.querySelector("[data-markdown-window-start]"), null);
67 console.log("markdown natural flow: worker races, stable DOM/selection, final references, cache and complete blocks passed");
68 } finally {
69 await act(async () => root.unmount()); worker.disposeMarkdownWorkerClient();
70 delete (globalThis as { Worker?: unknown }).Worker;
71 await server.close(); dom.window.close();
72 }
73
73 lines Plain Text