import { readFileSync } from "fs";
import { resolve } from "path";
/**
* These tests verify that CSS selectors in markdown.css correctly target
* the class names used by the Markdown component. When the p tag was changed
* to a div (to fix hydration errors), the CSS selectors must use class-only
* selectors instead of element-qualified selectors for paragraphs.
*/
const cssPath = resolve(__dirname, "../../css/markdown.css");
const tsxPath = resolve(__dirname, "Markdown.tsx");
const cssContent = readFileSync(cssPath, "utf-8");
const tsxContent = readFileSync(tsxPath, "utf-8");
describe("Markdown CSS/component selector consistency", () => {
it("should not use p.copilotKitMarkdownElement selector in CSS", () => {
// After the p->div change, CSS must not use element-qualified p selectors
expect(cssContent).not.toMatch(/\bp\.copilotKitMarkdownElement\b/);
});
it("should have .copilotKitParagraph selector in CSS for paragraph styling", () => {
expect(cssContent).toMatch(/\.copilotKitParagraph\s*\{/);
});
it("should have .copilotKitParagraph:not(:last-child) selector for paragraph spacing", () => {
expect(cssContent).toMatch(/\.copilotKitParagraph:not\(:last-child\)/);
});
it("should use copilotKitParagraph class on the paragraph component", () => {
// The p component override in Markdown.tsx should include copilotKitParagraph
expect(tsxContent).toMatch(/copilotKitParagraph/);
});
it("should render a div instead of p for the paragraph component", () => {
// The paragraph component should use
to avoid hydration errors
// when block-level elements are nested inside markdown paragraphs
const pComponentMatch = tsxContent.match(
/p:\s*\(\{[^}]*\}\)\s*=>\s*\(\s*<(\w+)/,
);
expect(pComponentMatch).not.toBeNull();
expect(pComponentMatch![1]).toBe("div");
});
it("should still have copilotKitMarkdownElement class on the paragraph div", () => {
// The div should retain the base class for any shared styling
const pSection = tsxContent.match(/p:\s*\([^)]*\)\s*=>\s*\([^)]+\)/s);
expect(pSection).not.toBeNull();
expect(pSection![0]).toContain("copilotKitMarkdownElement");
});
it("should use .copilotKitParagraph (not p) inside blockquote selector", () => {
// After p->div, blockquote nested paragraph selector must target the class
expect(cssContent).toMatch(
/blockquote\.copilotKitMarkdownElement\s+\.copilotKitParagraph\s*\{/,
);
expect(cssContent).not.toMatch(
/blockquote\.copilotKitMarkdownElement\s+p\s*\{/,
);
});
describe("other element selectors remain valid", () => {
const elementSelectors = [
{ element: "h1", selector: "h1.copilotKitMarkdownElement" },
{ element: "h2", selector: "h2.copilotKitMarkdownElement" },
{ element: "h3", selector: "h3.copilotKitMarkdownElement" },
{ element: "h4", selector: "h4.copilotKitMarkdownElement" },
{ element: "h5", selector: "h5.copilotKitMarkdownElement" },
{ element: "h6", selector: "h6.copilotKitMarkdownElement" },
{ element: "a", selector: "a.copilotKitMarkdownElement" },
{ element: "pre", selector: "pre.copilotKitMarkdownElement" },
{
element: "blockquote",
selector: "blockquote.copilotKitMarkdownElement",
},
{ element: "ul", selector: "ul.copilotKitMarkdownElement" },
{ element: "li", selector: "li.copilotKitMarkdownElement" },
];
for (const { element, selector } of elementSelectors) {
it(`should have ${element} component rendering <${element}> with copilotKitMarkdownElement class`, () => {
// Verify the component still uses the actual HTML element
// Some components use arrow syntax, others use function syntax
const arrowRegex = new RegExp(
`${element}:\\s*\\(\\{[^}]*\\}\\)\\s*=>\\s*\\(\\s*<${element}[\\s\\S]*?copilotKitMarkdownElement`,
);
const funcRegex = new RegExp(
`${element}\\([^)]*\\)\\s*\\{[\\s\\S]*?<${element}[\\s\\S]*?copilotKitMarkdownElement`,
);
const matches =
arrowRegex.test(tsxContent) || funcRegex.test(tsxContent);
expect(matches).toBe(true);
});
it(`should have CSS selector ${selector}`, () => {
expect(cssContent).toContain(selector);
});
}
});
});