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