import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { BlockQuote, CodeBlock, Heading, Link } from "../src/markdown";
describe("Heading", () => {
test("renders h1 with correct level", () => {
render(<Heading level={1}>Heading 1</Heading>);
const heading = screen.getByText("Heading 1");
expect(heading.tagName).toBe("H1");
expect(heading).toHaveClass("text-2xl", "font-bold");
});
test("renders h2 with correct level", () => {
render(<Heading level={2}>Heading 2</Heading>);
const heading = screen.getByText("Heading 2");
expect(heading.tagName).toBe("H1");
expect(heading).toHaveClass("text-xl", "font-bold");
});
test("renders h3 with correct level", () => {
render(<Heading level={3}>Heading 3</Heading>);
const heading = screen.getByText("Heading 3");
expect(heading.tagName).toBe("H1");
expect(heading).toHaveClass("text-lg", "font-bold");
});
test("renders h4 with correct level", () => {
render(<Heading level={4}>Heading 4</Heading>);
const heading = screen.getByText("Heading 4");
expect(heading.tagName).toBe("H1");
expect(heading).toHaveClass("text-md", "font-bold");
});
test("renders h5 with correct level", () => {
render(<Heading level={5}>Heading 5</Heading>);
const heading = screen.getByText("Heading 5");
expect(heading.tagName).toBe("H1");
expect(heading).toHaveClass("text-base", "font-bold");
});
test("renders h6 with correct level", () => {
render(<Heading level={6}>Heading 6</Heading>);
const heading = screen.getByText("Heading 6");
expect(heading.tagName).toBe("H1");
expect(heading).toHaveClass("text-base", "font-bold");
});
});
describe("BlockQuote", () => {
test("renders as blockquote element", () => {
render(<BlockQuote>Quote content</BlockQuote>);
const quote = screen.getByText("Quote content");
expect(quote.tagName).toBe("BLOCKQUOTE");
});
test("has left border style", () => {
render(<BlockQuote>Styled quote</BlockQuote>);
const quote = screen.getByText("Styled quote");
expect(quote).toHaveClass(
"border-l-4",
"border-gray-300",
"pl-4",
"italic",
);
});
test("renders with custom className", () => {
render(<BlockQuote className="custom-class">Custom</BlockQuote>);
const quote = screen.getByText("Custom");
expect(quote).toHaveClass("custom-class");
});
});
describe("Link", () => {
beforeEach(() => {
vi.spyOn(navigator.clipboard, "writeText").mockImplementation(vi.fn());
});
test("renders as anchor element", () => {
render(<Link href="https://example.com">Link Text</Link>);
const link = screen.getByText("Link Text");
expect(link.tagName).toBe("A");
expect(link).toHaveAttribute("href", "https://example.com");
});
test("opens in new tab by default", () => {
render(<Link href="https://example.com">New Tab</Link>);
const link = screen.getByText("New Tab");
expect(link).toHaveAttribute("target", "_blank");
});
test("uses provided target when specified", () => {
render(
<Link href="https://example.com" target="_self">
Same Tab
</Link>,
);
const link = screen.getByText("Same Tab");
expect(link).toHaveAttribute("target", "_self");
});
test("adds safe rel attributes for external links", () => {
render(<Link href="https://example.com">Safe Link</Link>);
const link = screen.getByText("Safe Link");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
test("preserves existing rel attributes", () => {
render(
<Link href="https://example.com" rel="custom-rel">
Custom Rel
</Link>,
);
const link = screen.getByText("Custom Rel");
expect(link).toHaveAttribute("rel", "noopener noreferrer custom-rel");
});
test("uses provided rel for non-external links", () => {
render(
<Link href="/local" target="_self" rel="custom">
Local Link
</Link>,
);
const link = screen.getByText("Local Link");
expect(link).toHaveAttribute("rel", "custom");
});
test("blocks javascript: href", () => {
render(<Link href="javascript:alert(1)">JS Link</Link>);
const link = screen.getByText("JS Link");
expect(link).not.toHaveAttribute("href");
});
test("blocks data: href", () => {
render(
<Link href="data:text/html,<script>alert(1)</script>">Data Link</Link>,
);
const link = screen.getByText("Data Link");
expect(link).not.toHaveAttribute("href");
});
test("blocks vbscript: href", () => {
render(<Link href="vbscript:alert(1)">VB Link</Link>);
const link = screen.getByText("VB Link");
expect(link).not.toHaveAttribute("href");
});
test("has hover underline style", () => {
render(<Link href="https://example.com">Underline Link</Link>);
const link = screen.getByText("Underline Link");
expect(link).toHaveClass("hover:underline", "underline-offset-1");
});
});
describe("CodeBlock", () => {
beforeEach(() => {
Object.defineProperty(navigator, "clipboard", {
value: { writeText: vi.fn() },
configurable: true,
});
});
test("renders code without language as inline code", () => {
render(<CodeBlock>inline code</CodeBlock>);
const code = screen.getByText("inline code");
expect(code.tagName).toBe("CODE");
expect(code).toHaveClass(
"rounded-md",
"px-1",
"py-0.5",
"text-[85%]",
"bg-gray-100",
"dark:bg-gray-800",
);
});
test("renders code with language as syntax highlighted block", () => {
render(
<CodeBlock className="language-javascript">
console.log("hello")
</CodeBlock>,
);
const codeContainer = document.querySelector(".overflow-x-auto");
expect(codeContainer).toBeInTheDocument();
});
test("shows language label for code block", () => {
render(<CodeBlock className="language-python">print('hello')</CodeBlock>);
expect(screen.getByText("python")).toBeInTheDocument();
});
test("has copy button in code block", () => {
render(<CodeBlock className="language-js">const x = 1;</CodeBlock>);
const copyButton = screen.getByRole("button", { name: "Copy" });
expect(copyButton).toBeInTheDocument();
});
test("copy button copies code content", () => {
render(<CodeBlock className="language-js">const x = 1;</CodeBlock>);
const copyButton = screen.getByRole("button", { name: "Copy" });
fireEvent.click(copyButton);
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("const x = 1;");
});
test("copy button shows Copied after clicking", () => {
render(<CodeBlock className="language-js">const x = 1;</CodeBlock>);
const copyButton = screen.getByRole("button", { name: "Copy" });
fireEvent.click(copyButton);
expect(screen.getByRole("button", { name: "Copied" })).toBeInTheDocument();
});
});