import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { cssDeclarations } from "./css-contract-test-support.ts"; const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); const markdownView = readFileSync(new URL("../src/components/chat-markdown-view.tsx", import.meta.url), "utf8"); const codeBlock = readFileSync(new URL("../src/components/markdown-code-block.tsx", import.meta.url), "utf8"); test("a fenced code block scrolls inside itself instead of spilling out of the message", () => { // There was no rule for fenced blocks at all. A bare
keeps
// `white-space: pre` with no overflow, so one long line pushed past the
// message column; the fix has to keep both properties together.
const body = cssDeclarations(".markdown-code pre > code", globalStyles);
assert.match(body, /overflow-x:\s*auto/);
assert.match(body, /white-space:\s*pre\b/);
assert.match(cssDeclarations(".markdown-code", globalStyles), /overflow:\s*hidden/);
});
test("the block resets the inline-code chip styling", () => {
// `.message-markdown code` paints a small chip for inline code. Inside a
// fence that chip has to go, or every block gets a nested background.
assert.match(cssDeclarations(".markdown-code pre > code", globalStyles), /background:\s*none/);
assert.match(cssDeclarations(".markdown-code pre", globalStyles), /background:\s*none/);
});
test("react-markdown routes fences through the block, not a bare pre", () => {
assert.match(markdownView, /pre: \(\{ children \}\) => /);
});
test("the copy control is labelled and the language is announced as text", () => {
assert.match(codeBlock, /aria-label=\{copied \? "已复制代码" : "复制代码"\}/);
assert.match(codeBlock, /language \|\| "代码"/);
// The copy target is the raw fence source, not rendered markup.
assert.match(codeBlock, /navigator\.clipboard\.writeText\(source\)/);
// A rejected clipboard permission must not flip the button to "copied".
assert.match(codeBlock, /catch \{\s*return;\s*\}/);
});
test("the copy timer is cleared when the block unmounts", () => {
// Streaming replaces these blocks constantly; a dangling timer would set
// state on an unmounted node.
assert.match(codeBlock, /useEffect\(\(\) => \(\) => \{[\s\S]*clearTimeout\(resetTimer\.current\)/);
});