import React, { act, useState } from "react"; (globalThis as Record).React = React; import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { renderToStaticMarkup } from "react-dom/server"; import { createRoot } from "react-dom/client"; import { parseHTML } from "linkedom"; import { VisualOutputCard } from "../components/artifacts/visual-card.tsx"; import { svgImageSource } from "../components/artifacts/svg-image.tsx"; import { MermaidOverlay, SessionMermaidContext } from "../components/session-overlay.tsx"; // Structure-only checks: linkedom does not load images or implement CSS cascade. // No fixture script, link, or external reference is executed by these tests. const FIGURE = `θ & φ: 50% #1`; const ACTIVE_MARKUP = `
Example
Link
`; const PREFIX = "data:image/svg+xml;charset=utf-8,"; function assertImageBoundary(holder: Element, source: string) { const image = holder.querySelector("img"); assert.ok(image, "figure is an image"); assert.equal(holder.children.length, 1, "no figure markup alongside the image"); assert.equal(holder.querySelector("svg, style, script, foreignObject, a, [onload], [onclick]"), null, "generated elements and CSS never enter the app document"); assert.equal(image.getAttribute("alt"), "Figure"); assert.equal(image.getAttribute("draggable"), "false", "native image dragging must not interfere with panning"); const src = image.getAttribute("src")!; assert.ok(src.startsWith(PREFIX)); assert.equal(decodeURIComponent(src.slice(PREFIX.length)), source, "image URL contains the expected SVG display source, not a raster"); return image; } describe("SVG display compatibility", () => { it("leaves standalone SVG unchanged, including comments and CDATA", () => { const source = FIGURE.replace("", ']]>'); assert.equal(svgImageSource(source), source); }); it("adds only a missing root namespace, ignoring comments and attribute values", () => { const source = '\' xmlns:xlink="http://www.w3.org/1999/xlink">'; const expected = source.replace('Example`; assert.equal(svgImageSource(source), source); } }); it("converts nbsp references in text and attributes, but not comments, CDATA or processing instructions", () => { const source = 'A B &nbsp;  &nbsp;'; const expected = source.replace('aria-label="A B"', 'aria-label="A B"').replace('A B', 'A B'); assert.equal(svgImageSource(source), expected); assert.equal(svgImageSource(expected), expected); }); }); describe("generated SVG image boundary", () => { for (const mode of ["inline", "fullscreen"] as const) { for (const [name, source] of [["reported style rule and fragment IDs", FIGURE], ["active markup", ACTIVE_MARKUP]] as const) { it(`${mode}: isolates ${name} inside the image URL`, () => { const html = renderToStaticMarkup(mode === "inline" ? : {}} />); const { document } = parseHTML(html); const holder = document.querySelector(mode === "inline" ? '[data-testid="visual-svg"]' : "[data-lh-overlay-chart]")!; assertImageBoundary(holder, source); assert.ok(holder.classList.contains("bg-white"), "white backing retained"); }); } for (const [name, original, display] of [ ["missing namespace", 'Example', 'Example'], ["HTML entity", 'A B', 'A B'], ] as const) { it(`${mode}: repairs ${name} only in the image source`, () => { const output = { svg: original }; const html = renderToStaticMarkup(mode === "inline" ? : {}} />); const { document } = parseHTML(html); const holder = document.querySelector(mode === "inline" ? '[data-testid="visual-svg"]' : "[data-lh-overlay-chart]")!; assertImageBoundary(holder, display); assert.equal(output.svg, original, "artifact source was not mutated"); }); } it(`${mode}: retains inversion on the white backing`, () => { const html = renderToStaticMarkup(mode === "inline" ? : {}} />); const { document } = parseHTML(html); const holder = document.querySelector(mode === "inline" ? '[data-testid="visual-svg"]' : "[data-lh-overlay-chart]")!; assert.match(holder.getAttribute("style")!, /invert\(1\) hue-rotate\(180deg\)/); assert.ok(holder.classList.contains("bg-white")); assertImageBoundary(holder, FIGURE); }); } it("inline: retains natural width and only shrinks oversized figures", () => { const { document } = parseHTML(renderToStaticMarkup()); const image = assertImageBoundary(document.querySelector('[data-testid="visual-svg"]')!, FIGURE); assert.equal(image.getAttribute("width"), "320"); assert.equal(image.className, "block h-auto max-w-full"); assert.ok(document.querySelector('[data-testid="visual-card"]')!.classList.contains("w-fit")); }); for (const width of ["", ' width="100%"']) { it(`inline: keeps the full-width fallback for ${width || "viewBox-only"} SVG`, () => { const source = `Example`; const { document } = parseHTML(renderToStaticMarkup()); const image = assertImageBoundary(document.querySelector('[data-testid="visual-svg"]')!, source); assert.equal(image.getAttribute("width"), null); assert.equal(image.className, "block h-auto w-full"); assert.ok(document.querySelector('[data-testid="visual-card"]')!.classList.contains("w-full")); }); } it("fullscreen: keeps the natural-size box and zoom controls", () => { const { document } = parseHTML(renderToStaticMarkup( {}} />)); const holder = document.querySelector("[data-lh-overlay-chart]")!; assert.equal(assertImageBoundary(holder, FIGURE).className, "block h-auto w-full"); assert.match(holder.parentElement!.getAttribute("style")!, /width:320px/); for (const label of ["Zoom in", "Zoom out", "Fit diagram to view", "Close"]) { assert.ok(document.querySelector(`button[aria-label="${label}"]`)); } }); }); it("the fullscreen button opens the same SVG image and inversion; download keeps original bytes", async () => { const originalSvg = FIGURE.replace(' xmlns="http://www.w3.org/2000/svg"', '').replace("θ & φ", "θ & φ"); const displaySvg = FIGURE.replace("θ & φ", "θ & φ"); const dom = parseHTML(""); const names = ["window", "document", "Element", "HTMLElement", "IS_REACT_ACT_ENVIRONMENT"] as const; const originals = names.map((name) => Object.getOwnPropertyDescriptor(globalThis, name)); Object.assign(globalThis, { window: dom.window, document: dom.document, Element: dom.window.Element, HTMLElement: dom.window.HTMLElement, IS_REACT_ACT_ENVIRONMENT: true, }); const host = dom.document.createElement("div"); dom.document.body.append(host); const root = createRoot(host); const originalCreateUrl = URL.createObjectURL; const originalRevokeUrl = URL.revokeObjectURL; const originalCreateElement = dom.document.createElement; let downloaded: Blob | undefined; let revoked: string | undefined; let clicked: { name: string; href: string } | undefined; URL.createObjectURL = (blob) => { assert.ok(blob instanceof Blob); downloaded = blob; return "blob:figure-test"; }; URL.revokeObjectURL = (url) => { revoked = url; }; function Harness() { const [focus, setFocus] = useState<{ svg: string; inverted: boolean } | null>(null); return ( {}, openFigure: (svg, inverted) => setFocus({ svg, inverted }) }}> {focus && setFocus(null)} />} ); } try { await act(async () => root.render()); await act(async () => { host.querySelector('[data-testid="visual-open"]')!.dispatchEvent(new dom.window.Event("click", { bubbles: true })); }); const inline = assertImageBoundary(host.querySelector('[data-testid="visual-svg"]')!, displaySvg); const overlayHolder = host.querySelector("[data-lh-overlay-chart]")!; const fullscreen = assertImageBoundary(overlayHolder, displaySvg); assert.equal(fullscreen.getAttribute("src"), inline.getAttribute("src")); assert.match(overlayHolder.getAttribute("style")!, /invert\(1\) hue-rotate\(180deg\)/); // Intercept only the download anchor; never navigate or write a real file. dom.document.createElement = ((...args: Parameters) => { const element = originalCreateElement.apply(dom.document, args); if (args[0] === "a") { element.click = () => { clicked = { name: element.getAttribute("download")!, href: element.getAttribute("href")! }; }; } return element; }) as typeof originalCreateElement; await act(async () => { host.querySelector('[data-testid="visual-download"]')!.dispatchEvent(new dom.window.Event("click", { bubbles: true })); }); assert.ok(downloaded); assert.equal(await downloaded.text(), originalSvg); assert.equal(downloaded.type, "image/svg+xml"); assert.deepEqual(clicked, { name: "figure.svg", href: "blob:figure-test" }); assert.equal(revoked, "blob:figure-test"); } finally { try { await act(async () => root.unmount()); } finally { URL.createObjectURL = originalCreateUrl; URL.revokeObjectURL = originalRevokeUrl; dom.document.createElement = originalCreateElement; host.remove(); for (const [i, name] of names.entries()) { if (originals[i]) Object.defineProperty(globalThis, name, originals[i]!); else Reflect.deleteProperty(globalThis, name); } } } });