返回 DeepSeek-Reasonix
markdown-idle-highlight.test.tsx
根目录 / desktop / frontend / src / __tests__ / markdown-idle-highlight.test.tsx
1 // Run: tsx src/__tests__/markdown-idle-highlight.test.tsx
2 //
3 // Oversized code blocks (≥ IDLE_HIGHLIGHT_MIN_BYTES, under the skip caps)
4 // mount the COMPLETE plain text immediately and swap in highlighted HTML from
5 // an idle callback; the skip caps remain the plain-forever policy. Small
6 // blocks keep synchronous highlighting. Text content is identical before and
7 // after the swap — nothing is ever truncated.
8
9 import { JSDOM } from "jsdom";
10 import React, { act } from "react";
11 import { createRoot } from "react-dom/client";
12 import HljsCode from "../components/editors/HljsCode";
13 import { IDLE_HIGHLIGHT_MIN_BYTES, MAX_HIGHLIGHT_BYTES } from "../lib/highlight";
14 import { LocaleProvider } from "../lib/i18n";
15
16 let passed = 0;
17 let failed = 0;
18
19 function ok(value: unknown, label: string) {
20 if (value) {
21 process.stdout.write(` PASS ${label}\n`);
22 passed += 1;
23 } else {
24 process.stdout.write(` FAIL ${label}\n`);
25 failed += 1;
26 }
27 }
28
29 function eq(actual: unknown, expected: unknown, label: string) {
30 if (actual === expected) ok(true, label);
31 else ok(false, `${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
32 }
33
34 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
35 pretendToBeVisual: true,
36 url: "http://localhost/",
37 });
38 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
39 globalThis.window = dom.window as unknown as Window & typeof globalThis;
40 globalThis.document = dom.window.document;
41 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
42 globalThis.HTMLElement = dom.window.HTMLElement;
43
44 let pendingIdle: Array<() => void> = [];
45 Object.defineProperty(dom.window, "requestIdleCallback", {
46 configurable: true,
47 value: (callback: () => void) => {
48 pendingIdle.push(callback);
49 return pendingIdle.length;
50 },
51 });
52 Object.defineProperty(dom.window, "cancelIdleCallback", {
53 configurable: true,
54 value: () => undefined,
55 });
56
57 async function runIdle() {
58 const callbacks = pendingIdle;
59 pendingIdle = [];
60 await act(async () => {
61 for (const callback of callbacks) callback();
62 await new Promise((resolve) => setTimeout(resolve, 0));
63 });
64 }
65
66 const rootEl = document.getElementById("root");
67 if (!rootEl) throw new Error("missing root");
68
69 function pre() {
70 return rootEl.querySelector("pre.code.hljs");
71 }
72
73 console.log("\nmarkdown idle highlight");
74
75 // ── oversized-but-highlightable block: plain first, highlight at idle ───────
76 {
77 const line = "const resultValue = computeSomething(argumentOne, argumentTwo); // comment\n";
78 const value = line.repeat(Math.ceil((IDLE_HIGHLIGHT_MIN_BYTES + 4096) / line.length));
79 ok(value.length > IDLE_HIGHLIGHT_MIN_BYTES, "fixture exceeds the idle-highlight threshold");
80 ok(value.length < MAX_HIGHLIGHT_BYTES, "fixture stays under the highlight skip cap");
81
82 const root = createRoot(rootEl);
83 await act(async () => {
84 root.render(
85 <LocaleProvider>
86 <HljsCode value={value} language="javascript" />
87 </LocaleProvider>,
88 );
89 });
90 eq(pre()?.getAttribute("data-highlight-mode"), "plain", "oversized block mounts as plain text");
91 eq(pre()?.textContent, value, "plain first paint carries the COMPLETE source");
92 ok(!pre()?.querySelector("span"), "plain first paint has no highlight markup");
93 ok(pendingIdle.length > 0, "highlight is scheduled for idle time");
94
95 await runIdle();
96 eq(pre()?.getAttribute("data-highlight-mode"), "syntax", "idle callback swaps in highlighted HTML");
97 ok(pre()?.querySelector("span"), "highlighted output contains token spans");
98 eq(pre()?.textContent, value, "text content is identical before and after highlighting");
99 await act(async () => root.unmount());
100 }
101
102 // ── small blocks keep synchronous highlighting ───────────────────────────────
103 {
104 const root = createRoot(rootEl);
105 await act(async () => {
106 root.render(
107 <LocaleProvider>
108 <HljsCode value="const small = true;" language="javascript" />
109 </LocaleProvider>,
110 );
111 });
112 eq(pre()?.getAttribute("data-highlight-mode"), "syntax", "small blocks highlight synchronously");
113 ok(pre()?.querySelector("span"), "small blocks render token spans immediately");
114 eq(pendingIdle.length, 0, "small blocks schedule no idle work");
115 await act(async () => root.unmount());
116 }
117
118 // ── over the skip caps: plain forever, still complete ────────────────────────
119 {
120 const value = "x = 1\n".repeat(Math.ceil((MAX_HIGHLIGHT_BYTES + 1024) / 6));
121 const root = createRoot(rootEl);
122 await act(async () => {
123 root.render(
124 <LocaleProvider>
125 <HljsCode value={value} language="python" />
126 </LocaleProvider>,
127 );
128 });
129 eq(pre()?.getAttribute("data-highlight-mode"), "plain", "over-cap blocks stay plain");
130 eq(pre()?.textContent, value, "over-cap blocks still show the complete source");
131 eq(pendingIdle.length, 0, "over-cap blocks schedule no idle highlight");
132 await runIdle();
133 eq(pre()?.getAttribute("data-highlight-mode"), "plain", "over-cap blocks remain plain after idle");
134 await act(async () => root.unmount());
135 }
136
137 dom.window.close();
138
139 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
140 if (failed > 0) process.exit(1);
141
141 lines Plain Text