| 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 |