Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions packages/ui/src/components/accordion.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "./accordion";

// jsdom does not implement PointerEvent, which Base UI dispatches on click.
if (typeof window.PointerEvent === "undefined") {
window.PointerEvent = class PointerEvent extends MouseEvent {} as typeof window.PointerEvent;
}

function renderAccordion(props?: React.ComponentProps<typeof Accordion>) {
return render(
<Accordion {...props}>
<AccordionItem value="a">
<AccordionTrigger>First</AccordionTrigger>
<AccordionContent>First panel body</AccordionContent>
</AccordionItem>
<AccordionItem value="b">
<AccordionTrigger>Second</AccordionTrigger>
<AccordionContent>Second panel body</AccordionContent>
</AccordionItem>
</Accordion>
);
}

describe("Accordion", () => {
it("renders a trigger per item", () => {
renderAccordion();
expect(screen.getByRole("button", { name: "First" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Second" })).toBeInTheDocument();
});

it("starts collapsed", () => {
renderAccordion();
expect(screen.getByRole("button", { name: "First" })).toHaveAttribute(
"aria-expanded",
"false"
);
});

it("expands a panel when its trigger is clicked", async () => {
const user = userEvent.setup();
renderAccordion();

const trigger = screen.getByRole("button", { name: "First" });
await user.click(trigger);

expect(trigger).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText("First panel body")).toBeVisible();
});

it("collapses an expanded panel when its trigger is clicked again", async () => {
const user = userEvent.setup();
renderAccordion({ defaultValue: ["a"] });

const trigger = screen.getByRole("button", { name: "First" });
expect(trigger).toHaveAttribute("aria-expanded", "true");

await user.click(trigger);
expect(trigger).toHaveAttribute("aria-expanded", "false");
});

it("fires onValueChange with the opened item's value", async () => {
const user = userEvent.setup();
const onValueChange = vi.fn();
renderAccordion({ onValueChange });

await user.click(screen.getByRole("button", { name: "Second" }));

expect(onValueChange).toHaveBeenCalled();
expect(onValueChange.mock.calls[0][0]).toContain("b");
});

it("carries data-slot attributes", () => {
const { container } = renderAccordion();
expect(container.querySelector('[data-slot="accordion"]')).toBeInTheDocument();
expect(container.querySelector('[data-slot="accordion-item"]')).toBeInTheDocument();
expect(
container.querySelector('[data-slot="accordion-trigger"]')
).toBeInTheDocument();
});

it("merges a consumer className on the root", () => {
const { container } = renderAccordion({ className: "my-accordion" });
expect(container.querySelector('[data-slot="accordion"]')).toHaveClass(
"my-accordion"
);
});
});
23 changes: 23 additions & 0 deletions packages/ui/src/components/api-method.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ describe("ParamRow", () => {
expect(screen.getByText("string | Buffer | URL")).toBeInTheDocument();
expect(screen.getByText("Filename to read.")).toBeInTheDocument();
});

it("omits the description span when no description is given", () => {
render(<ParamRow name="signal" type="AbortSignal" />);
expect(screen.getByText("signal")).toBeInTheDocument();
expect(screen.getByText("AbortSignal")).toBeInTheDocument();
});
});

describe("ApiMethod", () => {
Expand Down Expand Up @@ -45,4 +51,21 @@ describe("ApiMethod", () => {
render(<ApiMethod signature="fs.readFile()" example={<div>{'console.log("hi")'}</div>} />);
expect(screen.getByText('console.log("hi")')).toBeInTheDocument();
});

it("renders a non-string (ReactNode) signature as-is and labels the anchor generically", () => {
render(
<ApiMethod signature={<span data-testid="sig">custom signature node</span>} anchorId="custom" />,
);
expect(screen.getByTestId("sig")).toBeInTheDocument();
// emphasizeSignature can't split a node, so the anchor uses the fallback name.
expect(screen.getByRole("link", { name: "Link to this method" })).toHaveAttribute(
"href",
"#custom",
);
});

it("leaves a signature with no parenthesis unsplit", () => {
const { container } = render(<ApiMethod signature="fs.constants" />);
expect(container).toHaveTextContent("fs.constants");
});
});
54 changes: 54 additions & 0 deletions packages/ui/src/components/breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Breadcrumbs } from "./breadcrumbs";

const segments = [
{ label: "Runtime", href: "#runtime" },
{ label: "Guides" }, // no href → plain text
{ label: "Debugging" }, // last → current page
];

describe("Breadcrumbs", () => {
it("renders the segments as an ordered list", () => {
render(<Breadcrumbs segments={segments} />);
const list = screen.getByRole("list");
expect(list.tagName).toBe("OL");
expect(screen.getAllByRole("listitem")).toHaveLength(3);
});

it("marks the last segment as the current page and does not link it", () => {
render(<Breadcrumbs segments={segments} />);
const current = screen.getByText("Debugging");
expect(current).toHaveAttribute("aria-current", "page");
expect(current.tagName).toBe("SPAN");
expect(screen.queryByRole("link", { name: "Debugging" })).not.toBeInTheDocument();
});

it("renders segments with an href as links", () => {
render(<Breadcrumbs segments={segments} />);
const link = screen.getByRole("link", { name: "Runtime" });
expect(link).toHaveAttribute("href", "#runtime");
});

it("renders segments without an href as plain text", () => {
render(<Breadcrumbs segments={segments} />);
const guides = screen.getByText("Guides");
expect(guides.tagName).toBe("SPAN");
expect(guides).not.toHaveAttribute("aria-current");
expect(screen.queryByRole("link", { name: "Guides" })).not.toBeInTheDocument();
});

it("renders a separator between segments, hidden from assistive tech", () => {
const { container } = render(<Breadcrumbs segments={segments} />);
const separators = container.querySelectorAll("svg[aria-hidden]");
// One separator after every segment except the last.
expect(separators).toHaveLength(segments.length - 1);
});

it("merges a consumer className onto the nav", () => {
render(<Breadcrumbs segments={segments} className="custom-x" />);
const nav = screen.getByRole("navigation", { name: "Breadcrumb" });
expect(nav).toHaveClass("custom-x");
expect(nav).toHaveClass("text-sm");
});
});
37 changes: 37 additions & 0 deletions packages/ui/src/components/callout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Callout } from "./callout";

describe("Callout", () => {
it("renders as a note landmark with its children", () => {
render(<Callout>Body content here.</Callout>);
expect(screen.getByRole("note")).toBeInTheDocument();
expect(screen.getByText("Body content here.")).toBeInTheDocument();
});

it("defaults to the Note tone label", () => {
render(<Callout>x</Callout>);
expect(screen.getByText("Note")).toBeInTheDocument();
});

it("uses the tone's label for other tones", () => {
render(<Callout tone="warning">x</Callout>);
expect(screen.getByText("Warning")).toBeInTheDocument();
expect(screen.queryByText("Note")).not.toBeInTheDocument();
});

it("renders a title override in place of the default label", () => {
render(
<Callout tone="tip" title="Custom heading">
x
</Callout>,
);
expect(screen.getByText("Custom heading")).toBeInTheDocument();
expect(screen.queryByText("Tip")).not.toBeInTheDocument();
});

it("merges a consumer className", () => {
render(<Callout className="custom-x">x</Callout>);
expect(screen.getByRole("note")).toHaveClass("custom-x");
});
});
71 changes: 71 additions & 0 deletions packages/ui/src/components/code-block.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { CodeBlock } from "./code-block";

describe("CodeBlock — editor variant", () => {
it("renders the filename, a line-number gutter, a vim status bar, and a copy button", () => {
const code = "const a = 1;\nconst b = 2;\nconst c = 3;";
const { container } = render(
<CodeBlock variant="editor" filename="app.ts" lang="typescript" code={code} />,
);

// Filename shows in the title bar and again in the status bar.
expect(screen.getAllByText("app.ts").length).toBeGreaterThanOrEqual(1);

// The gutter is the aria-hidden div listing one number per code line.
const gutter = container.querySelector("div[aria-hidden]");
expect(gutter).not.toBeNull();
expect(gutter!.textContent!.split("\n")).toEqual(["1", "2", "3"]);

// Vim status bar.
expect(screen.getByText("NORMAL")).toBeInTheDocument();
expect(container).toHaveTextContent("utf-8 · typescript · 3L");

// Icon-only copy button, named from the localized "Copy".
expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument();
});

it("hides the gutter and status bar when lineNumbers/statusBar are false", () => {
const { container } = render(
<CodeBlock variant="editor" filename="app.ts" lang="ts" code={"a\nb"} lineNumbers={false} statusBar={false} />,
);
expect(container.querySelector("div[aria-hidden]")).toBeNull();
expect(screen.queryByText("NORMAL")).not.toBeInTheDocument();
});

it("renders traffic lights", () => {
const { container } = render(<CodeBlock code={"x"} filename="a.ts" />);
expect(container.querySelector('[class*="ff5f56"]')).not.toBeNull();
});
});

describe("CodeBlock — terminal variant", () => {
it("renders the lang as the title row and no traffic lights", () => {
const { container } = render(<CodeBlock variant="terminal" lang="bash" code={"echo hi"} />);
expect(screen.getByText("bash")).toBeInTheDocument();
expect(container.querySelector('[class*="ff5f56"]')).toBeNull();
});

it("falls back to the localized TERMINAL title when no lang is given", () => {
render(<CodeBlock variant="terminal" code={"echo hi"} />);
expect(screen.getByText("Terminal")).toBeInTheDocument();
});
});

describe("CodeBlock — body", () => {
it("renders `children` in place of the prism-highlighted body", () => {
render(
<CodeBlock code={"const ignored = true;"}>
<span>custom pre-highlighted body</span>
</CodeBlock>,
);
expect(screen.getByText("custom pre-highlighted body")).toBeInTheDocument();
});

it("renders one line span per code line even when lines are identical", () => {
const { container } = render(<CodeBlock code={"dup\ndup\ndup"} />);
// Each highlighted line is a `span.block`; keyed() keeps the React keys
// unique across the duplicate content, so all three render.
expect(container.querySelectorAll("span.block")).toHaveLength(3);
});
});
97 changes: 97 additions & 0 deletions packages/ui/src/components/command-palette.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { CommandPalette, type CommandGroup } from "./command-palette";

function makeGroups(overrides?: {
home?: () => void;
docs?: () => void;
python?: () => void;
}): CommandGroup[] {
return [
{
heading: "Pages",
items: [
{ id: "home", title: "Home", onSelect: overrides?.home },
{ id: "docs", title: "Docs", subtitle: "Read the docs", onSelect: overrides?.docs },
],
},
{
heading: "Languages",
items: [{ id: "py", title: "Python", keywords: "python snake", onSelect: overrides?.python }],
},
];
}

describe("CommandPalette", () => {
it("renders nothing while closed", () => {
render(<CommandPalette open={false} groups={makeGroups()} />);
expect(screen.queryByRole("button", { name: /Home/ })).not.toBeInTheDocument();
});

it("renders the search input, group headings, and items when open", () => {
render(<CommandPalette open groups={makeGroups()} placeholder="Find anything" />);
expect(screen.getByRole("textbox", { name: "Find anything" })).toBeInTheDocument();
expect(screen.getByText("Pages")).toBeInTheDocument();
expect(screen.getByText("Languages")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Home/ })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Python/ })).toBeInTheDocument();
});

it("filters items as the user types", async () => {
const user = userEvent.setup();
render(<CommandPalette open groups={makeGroups()} />);

await user.type(screen.getByRole("textbox"), "python");

expect(screen.getByRole("button", { name: /Python/ })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /Home/ })).not.toBeInTheDocument();
// The empty "Pages" group is dropped entirely.
expect(screen.queryByText("Pages")).not.toBeInTheDocument();
});

it("shows the empty message when nothing matches", async () => {
const user = userEvent.setup();
render(<CommandPalette open groups={makeGroups()} emptyMessage="Nothing here" />);

await user.type(screen.getByRole("textbox"), "zzzzz");

expect(screen.getByText("Nothing here")).toBeInTheDocument();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});

it("fires the item's onSelect and requests close when clicked", async () => {
const user = userEvent.setup();
const home = vi.fn();
const onOpenChange = vi.fn();
render(<CommandPalette open onOpenChange={onOpenChange} groups={makeGroups({ home })} />);

await user.click(screen.getByRole("button", { name: /Home/ }));

expect(home).toHaveBeenCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(false);
});

it("selects the item under the keyboard cursor on Enter", async () => {
const user = userEvent.setup();
const home = vi.fn();
const docs = vi.fn();
render(<CommandPalette open groups={makeGroups({ home, docs })} />);

// Cursor starts on the first item (Home); ArrowDown moves to Docs.
await user.keyboard("{ArrowDown}{Enter}");

expect(docs).toHaveBeenCalledTimes(1);
expect(home).not.toHaveBeenCalled();
});

it("wraps to the last item on ArrowUp from the top", async () => {
const user = userEvent.setup();
const python = vi.fn();
render(<CommandPalette open groups={makeGroups({ python })} />);

await user.keyboard("{ArrowUp}{Enter}");

expect(python).toHaveBeenCalledTimes(1);
});
});
Loading
Loading