test: add unit and component tests across the monorepo

Adds ~1000 tests to bring the previously-untested packages and apps
under coverage:

- packages/utils — string, color, date, file, level, locale, sanitize,
  field, html, network-icons, rate-limit, url, url-security, monorepo,
  resume/patch, resume/docx/link-utils, style helpers (~97% on the
  testable utility files)
- packages/ui — 28 component test files plus the use-controlled-state,
  use-mobile, use-confirm, use-prompt hooks (95% statements)
- packages/pdf — shared template helpers (filtering, columns, picture,
  metrics, section-links, page-size, rich-text-html, section-title)
- packages/schema — resumeDataSchema, page, templates, default
- packages/fonts — expanded coverage on font helpers
- packages/ai — resume sanitize and extraction template
- packages/api — resume-access-policy
- apps/web — error-message, locale, theme, pwa, dialogs/store, and
  the resume/move-item / section-actions / make-section-item helpers

Adds jsdom polyfills (ResizeObserver, IntersectionObserver,
scrollIntoView, matchMedia) and an explicit React Testing Library
cleanup hook to vitest.setup.ts so portal- and overlay-based components
work without per-test setup.
This commit is contained in:
Amruth Pillai
2026-05-10 20:00:07 +02:00
parent 33103536ae
commit 6a01207b6b
83 changed files with 8099 additions and 6 deletions
@@ -0,0 +1,107 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
describe("Accordion", () => {
it("renders with data-slot='accordion'", () => {
render(
<Accordion data-testid="acc">
<AccordionItem value="1">
<AccordionTrigger>Q1</AccordionTrigger>
<AccordionContent>A1</AccordionContent>
</AccordionItem>
</Accordion>,
);
expect(screen.getByTestId("acc")).toHaveAttribute("data-slot", "accordion");
});
it("merges custom className", () => {
render(
<Accordion data-testid="acc" className="my-class">
<AccordionItem value="1">
<AccordionTrigger>Q</AccordionTrigger>
<AccordionContent>A</AccordionContent>
</AccordionItem>
</Accordion>,
);
expect(screen.getByTestId("acc")).toHaveClass("my-class");
});
it("expands and collapses on trigger click", async () => {
render(
<Accordion>
<AccordionItem value="1">
<AccordionTrigger>Question 1</AccordionTrigger>
<AccordionContent>Answer 1</AccordionContent>
</AccordionItem>
</Accordion>,
);
const trigger = screen.getByText("Question 1");
expect(trigger.getAttribute("aria-expanded")).toBe("false");
await userEvent.click(trigger);
expect(trigger.getAttribute("aria-expanded")).toBe("true");
await userEvent.click(trigger);
expect(trigger.getAttribute("aria-expanded")).toBe("false");
});
it("renders multiple items", () => {
render(
<Accordion>
<AccordionItem value="1">
<AccordionTrigger>Q1</AccordionTrigger>
<AccordionContent>A1</AccordionContent>
</AccordionItem>
<AccordionItem value="2">
<AccordionTrigger>Q2</AccordionTrigger>
<AccordionContent>A2</AccordionContent>
</AccordionItem>
</Accordion>,
);
expect(screen.getByText("Q1")).toBeInTheDocument();
expect(screen.getByText("Q2")).toBeInTheDocument();
});
});
describe("AccordionItem", () => {
it("uses data-slot='accordion-item'", () => {
render(
<Accordion>
<AccordionItem value="1" data-testid="item">
<AccordionTrigger>Q</AccordionTrigger>
<AccordionContent>A</AccordionContent>
</AccordionItem>
</Accordion>,
);
expect(screen.getByTestId("item")).toHaveAttribute("data-slot", "accordion-item");
});
});
describe("AccordionTrigger", () => {
it("uses data-slot='accordion-trigger'", () => {
render(
<Accordion>
<AccordionItem value="1">
<AccordionTrigger data-testid="trigger">Q</AccordionTrigger>
<AccordionContent>A</AccordionContent>
</AccordionItem>
</Accordion>,
);
expect(screen.getByTestId("trigger")).toHaveAttribute("data-slot", "accordion-trigger");
});
it("renders down/up caret icons", () => {
const { container } = render(
<Accordion>
<AccordionItem value="1">
<AccordionTrigger>Q</AccordionTrigger>
<AccordionContent>A</AccordionContent>
</AccordionItem>
</Accordion>,
);
expect(container.querySelectorAll("[data-slot=accordion-trigger-icon]")).toHaveLength(2);
});
});
@@ -0,0 +1,126 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "./alert-dialog";
describe("AlertDialog", () => {
it("does not render content by default", () => {
render(
<AlertDialog>
<AlertDialogTrigger>Open</AlertDialogTrigger>
<AlertDialogContent>Hidden body</AlertDialogContent>
</AlertDialog>,
);
expect(screen.queryByText("Hidden body")).not.toBeInTheDocument();
});
it("opens via defaultOpen", () => {
render(
<AlertDialog defaultOpen>
<AlertDialogTrigger>Open</AlertDialogTrigger>
<AlertDialogContent>Visible body</AlertDialogContent>
</AlertDialog>,
);
expect(screen.getByText("Visible body")).toBeInTheDocument();
});
it("trigger has data-slot='alert-dialog-trigger'", () => {
render(
<AlertDialog>
<AlertDialogTrigger data-testid="trigger">Open</AlertDialogTrigger>
<AlertDialogContent>x</AlertDialogContent>
</AlertDialog>,
);
expect(screen.getByTestId("trigger")).toHaveAttribute("data-slot", "alert-dialog-trigger");
});
it("content uses size 'default' by default", () => {
render(
<AlertDialog open>
<AlertDialogTrigger>x</AlertDialogTrigger>
<AlertDialogContent>visible</AlertDialogContent>
</AlertDialog>,
);
expect(screen.getByText("visible")).toHaveAttribute("data-size", "default");
});
it("content supports size='sm'", () => {
render(
<AlertDialog open>
<AlertDialogTrigger>x</AlertDialogTrigger>
<AlertDialogContent size="sm">visible</AlertDialogContent>
</AlertDialog>,
);
expect(screen.getByText("visible")).toHaveAttribute("data-size", "sm");
});
});
describe("AlertDialog subcomponents", () => {
it("AlertDialogHeader uses data-slot='alert-dialog-header'", () => {
render(<AlertDialogHeader data-testid="h">x</AlertDialogHeader>);
expect(screen.getByTestId("h")).toHaveAttribute("data-slot", "alert-dialog-header");
});
it("AlertDialogFooter uses data-slot='alert-dialog-footer'", () => {
render(<AlertDialogFooter data-testid="f">x</AlertDialogFooter>);
expect(screen.getByTestId("f")).toHaveAttribute("data-slot", "alert-dialog-footer");
});
it("AlertDialogMedia uses data-slot='alert-dialog-media'", () => {
render(<AlertDialogMedia data-testid="m">x</AlertDialogMedia>);
expect(screen.getByTestId("m")).toHaveAttribute("data-slot", "alert-dialog-media");
});
it("AlertDialogTitle uses data-slot='alert-dialog-title'", () => {
render(
<AlertDialog open>
<AlertDialogTrigger>x</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
</AlertDialogContent>
</AlertDialog>,
);
expect(screen.getByText("Are you sure?")).toHaveAttribute("data-slot", "alert-dialog-title");
});
it("AlertDialogDescription uses data-slot='alert-dialog-description'", () => {
render(
<AlertDialog open>
<AlertDialogTrigger>x</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogDescription>This is permanent</AlertDialogDescription>
</AlertDialogContent>
</AlertDialog>,
);
expect(screen.getByText("This is permanent")).toHaveAttribute("data-slot", "alert-dialog-description");
});
it("AlertDialogAction renders a Button with data-slot='alert-dialog-action'", () => {
render(<AlertDialogAction>Confirm</AlertDialogAction>);
const button = screen.getByText("Confirm");
expect(button).toHaveAttribute("data-slot", "alert-dialog-action");
});
it("AlertDialogCancel uses data-slot='alert-dialog-cancel'", () => {
render(
<AlertDialog open>
<AlertDialogTrigger>x</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogCancel>Cancel</AlertDialogCancel>
</AlertDialogContent>
</AlertDialog>,
);
const button = screen.getByText("Cancel");
expect(button).toHaveAttribute("data-slot", "alert-dialog-cancel");
});
});
+75
View File
@@ -0,0 +1,75 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Alert, AlertAction, AlertDescription, AlertTitle } from "./alert";
describe("Alert", () => {
it("renders children with role='alert'", () => {
render(<Alert>Heads up!</Alert>);
expect(screen.getByRole("alert")).toHaveTextContent("Heads up!");
});
it("applies data-slot='alert' for slotting", () => {
render(<Alert>x</Alert>);
expect(screen.getByRole("alert")).toHaveAttribute("data-slot", "alert");
});
it("merges custom className", () => {
render(<Alert className="my-class">x</Alert>);
expect(screen.getByRole("alert")).toHaveClass("my-class");
});
it("supports destructive variant", () => {
render(<Alert variant="destructive">x</Alert>);
// We just verify the component renders without error
expect(screen.getByRole("alert")).toBeInTheDocument();
});
});
describe("AlertTitle", () => {
it("renders children", () => {
render(<AlertTitle>Title</AlertTitle>);
expect(screen.getByText("Title")).toBeInTheDocument();
});
it("applies data-slot='alert-title'", () => {
render(<AlertTitle>x</AlertTitle>);
expect(screen.getByText("x")).toHaveAttribute("data-slot", "alert-title");
});
});
describe("AlertDescription", () => {
it("renders children", () => {
render(<AlertDescription>Description text</AlertDescription>);
expect(screen.getByText("Description text")).toBeInTheDocument();
});
it("applies data-slot='alert-description'", () => {
render(<AlertDescription>x</AlertDescription>);
expect(screen.getByText("x")).toHaveAttribute("data-slot", "alert-description");
});
});
describe("AlertAction", () => {
it("renders children", () => {
render(<AlertAction>Action</AlertAction>);
expect(screen.getByText("Action")).toBeInTheDocument();
});
it("applies data-slot='alert-action'", () => {
render(<AlertAction>x</AlertAction>);
expect(screen.getByText("x")).toHaveAttribute("data-slot", "alert-action");
});
});
describe("Alert composition", () => {
it("composes all subcomponents", () => {
render(
<Alert>
<AlertTitle>Title</AlertTitle>
<AlertDescription>Body</AlertDescription>
<AlertAction>OK</AlertAction>
</Alert>,
);
expect(screen.getByRole("alert")).toHaveTextContent(/TitleBodyOK/);
});
});
@@ -0,0 +1,88 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage } from "./avatar";
describe("Avatar", () => {
it("renders with data-slot='avatar'", () => {
render(<Avatar data-testid="av" />);
expect(screen.getByTestId("av")).toHaveAttribute("data-slot", "avatar");
});
it("defaults size to 'default'", () => {
render(<Avatar data-testid="av" />);
expect(screen.getByTestId("av")).toHaveAttribute("data-size", "default");
});
it.each(["default", "sm", "lg"] as const)("supports size=%s", (size) => {
render(<Avatar data-testid="av" size={size} />);
expect(screen.getByTestId("av")).toHaveAttribute("data-size", size);
});
it("merges custom className", () => {
render(<Avatar data-testid="av" className="my-class" />);
expect(screen.getByTestId("av")).toHaveClass("my-class");
});
});
describe("AvatarFallback", () => {
it("renders fallback content", () => {
render(
<Avatar>
<AvatarFallback data-testid="fb">JD</AvatarFallback>
</Avatar>,
);
// Note: fallback only renders if image fails — but data-slot should be set.
// We just confirm no crash and that fallback is present in DOM.
const fb = screen.queryByTestId("fb");
// AvatarFallback may or may not be in the DOM depending on image state.
// Just ensure it doesn't throw on render.
expect(fb === null || fb.getAttribute("data-slot") === "avatar-fallback").toBe(true);
});
});
describe("AvatarBadge", () => {
it("renders as span with data-slot='avatar-badge'", () => {
render(<AvatarBadge data-testid="b" />);
const badge = screen.getByTestId("b");
expect(badge.tagName).toBe("SPAN");
expect(badge).toHaveAttribute("data-slot", "avatar-badge");
});
});
describe("AvatarGroup", () => {
it("renders as div with data-slot='avatar-group'", () => {
render(<AvatarGroup data-testid="g" />);
const group = screen.getByTestId("g");
expect(group.tagName).toBe("DIV");
expect(group).toHaveAttribute("data-slot", "avatar-group");
});
it("supports children", () => {
render(
<AvatarGroup data-testid="g">
<Avatar />
<Avatar />
</AvatarGroup>,
);
expect(screen.getByTestId("g").children).toHaveLength(2);
});
});
describe("AvatarGroupCount", () => {
it("renders count children", () => {
render(<AvatarGroupCount>+3</AvatarGroupCount>);
expect(screen.getByText("+3")).toHaveAttribute("data-slot", "avatar-group-count");
});
});
describe("AvatarImage", () => {
it("accepts src prop without throwing", () => {
render(
<Avatar>
<AvatarImage src="https://example.com/x.png" alt="user" />
</Avatar>,
);
// Image may not render until loaded; we just verify no crash
expect(true).toBe(true);
});
});
+39
View File
@@ -0,0 +1,39 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Badge } from "./badge";
describe("Badge", () => {
it("renders children inside a span by default", () => {
render(<Badge>New</Badge>);
const badge = screen.getByText("New");
expect(badge.tagName).toBe("SPAN");
});
it("merges custom className", () => {
render(<Badge className="my-extra">x</Badge>);
expect(screen.getByText("x")).toHaveClass("my-extra");
});
it.each([
["default"],
["secondary"],
["destructive"],
["outline"],
["ghost"],
["link"],
] as const)("renders variant=%s without throwing", (variant) => {
render(<Badge variant={variant}>{variant}</Badge>);
expect(screen.getByText(variant)).toBeInTheDocument();
});
it("forwards aria attributes", () => {
render(<Badge aria-label="status indicator">3</Badge>);
expect(screen.getByLabelText("status indicator")).toBeInTheDocument();
});
it("supports a custom render function", () => {
render(<Badge render={(props) => <a {...props} href="/x" />}>link</Badge>);
const anchor = screen.getByRole("link", { name: "link" });
expect(anchor).toBeInTheDocument();
});
});
@@ -0,0 +1,41 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { BrandIcon } from "./brand-icon";
describe("BrandIcon", () => {
it("renders two img elements (light + dark variants)", () => {
render(<BrandIcon />);
const imgs = screen.getAllByAltText("Reactive Resume");
expect(imgs).toHaveLength(2);
});
it("uses 'logo' as default variant", () => {
render(<BrandIcon />);
const imgs = screen.getAllByAltText("Reactive Resume");
expect(imgs.some((img) => img.getAttribute("src") === "/logo/dark.svg")).toBe(true);
expect(imgs.some((img) => img.getAttribute("src") === "/logo/light.svg")).toBe(true);
});
it("uses 'icon' variant when specified", () => {
render(<BrandIcon variant="icon" />);
const imgs = screen.getAllByAltText("Reactive Resume");
expect(imgs.some((img) => img.getAttribute("src") === "/icon/dark.svg")).toBe(true);
expect(imgs.some((img) => img.getAttribute("src") === "/icon/light.svg")).toBe(true);
});
it("merges custom className on both imgs", () => {
render(<BrandIcon className="my-custom" />);
const imgs = screen.getAllByAltText("Reactive Resume");
for (const img of imgs) {
expect(img).toHaveClass("my-custom");
}
});
it("hides dark variant by default (light mode); dark mode reveals it via dark:block", () => {
render(<BrandIcon />);
const imgs = screen.getAllByAltText("Reactive Resume");
const darkImg = imgs.find((img) => img.getAttribute("src") === "/logo/dark.svg");
expect(darkImg).toHaveClass("hidden");
expect(darkImg).toHaveClass("dark:block");
});
});
@@ -0,0 +1,61 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "./button-group";
describe("ButtonGroup", () => {
it("renders a fieldset with data-slot='button-group'", () => {
render(<ButtonGroup data-testid="g" />);
const g = screen.getByTestId("g");
expect(g.tagName).toBe("FIELDSET");
expect(g).toHaveAttribute("data-slot", "button-group");
});
it("respects horizontal orientation (default)", () => {
render(<ButtonGroup data-testid="g" orientation="horizontal" />);
expect(screen.getByTestId("g")).toHaveAttribute("data-orientation", "horizontal");
});
it("respects vertical orientation", () => {
render(<ButtonGroup data-testid="g" orientation="vertical" />);
expect(screen.getByTestId("g")).toHaveAttribute("data-orientation", "vertical");
});
it("merges custom className", () => {
render(<ButtonGroup data-testid="g" className="my-custom" />);
expect(screen.getByTestId("g")).toHaveClass("my-custom");
});
});
describe("ButtonGroupText", () => {
it("renders content inside a div by default", () => {
render(<ButtonGroupText>Hello</ButtonGroupText>);
expect(screen.getByText("Hello").tagName).toBe("DIV");
});
it("merges custom className", () => {
render(<ButtonGroupText className="my-class">x</ButtonGroupText>);
expect(screen.getByText("x")).toHaveClass("my-class");
});
it("supports custom render function", () => {
render(<ButtonGroupText render={(props) => <span {...props} />}>label</ButtonGroupText>);
expect(screen.getByText("label").tagName).toBe("SPAN");
});
});
describe("ButtonGroupSeparator", () => {
it("defaults to vertical orientation", () => {
render(<ButtonGroupSeparator data-testid="sep" />);
expect(screen.getByTestId("sep")).toHaveAttribute("data-orientation", "vertical");
});
it("supports horizontal orientation", () => {
render(<ButtonGroupSeparator data-testid="sep" orientation="horizontal" />);
expect(screen.getByTestId("sep")).toHaveAttribute("data-orientation", "horizontal");
});
it("uses data-slot='button-group-separator'", () => {
render(<ButtonGroupSeparator data-testid="sep" />);
expect(screen.getByTestId("sep")).toHaveAttribute("data-slot", "button-group-separator");
});
});
@@ -0,0 +1,85 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Button } from "./button";
describe("Button", () => {
it("renders children", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
});
it("defaults to type='button' to prevent accidental form submits", () => {
render(<Button>Submit</Button>);
expect(screen.getByRole("button")).toHaveAttribute("type", "button");
});
it("respects an explicit type override", () => {
render(<Button type="submit">Submit</Button>);
expect(screen.getByRole("button")).toHaveAttribute("type", "submit");
});
it("applies the data-slot attribute for shadcn/base-ui slotting", () => {
render(<Button>x</Button>);
expect(screen.getByRole("button")).toHaveAttribute("data-slot", "button");
});
it("merges custom className with variant classes", () => {
render(<Button className="my-custom-class">x</Button>);
expect(screen.getByRole("button")).toHaveClass("my-custom-class");
});
it("calls onClick when clicked", async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click</Button>);
await userEvent.click(screen.getByRole("button"));
expect(onClick).toHaveBeenCalledOnce();
});
it("does not fire onClick when disabled", async () => {
const onClick = vi.fn();
render(
<Button disabled onClick={onClick}>
Click
</Button>,
);
await userEvent.click(screen.getByRole("button"));
expect(onClick).not.toHaveBeenCalled();
});
it("renders disabled state", () => {
render(<Button disabled>x</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
it.each([
["default"],
["outline"],
["secondary"],
["ghost"],
["destructive"],
["link"],
] as const)("renders variant=%s without throwing", (variant) => {
render(<Button variant={variant}>x</Button>);
expect(screen.getByRole("button")).toBeInTheDocument();
});
it.each([
["default"],
["xs"],
["sm"],
["lg"],
["icon"],
["icon-xs"],
["icon-sm"],
["icon-lg"],
] as const)("renders size=%s without throwing", (size) => {
render(<Button size={size}>x</Button>);
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("forwards aria-label", () => {
render(<Button aria-label="Close">×</Button>);
expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
});
});
+99
View File
@@ -0,0 +1,99 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
describe("Card", () => {
it("renders with data-slot='card'", () => {
render(<Card data-testid="c">x</Card>);
expect(screen.getByTestId("c")).toHaveAttribute("data-slot", "card");
});
it("defaults size to 'default'", () => {
render(<Card data-testid="c">x</Card>);
expect(screen.getByTestId("c")).toHaveAttribute("data-size", "default");
});
it("supports size='sm'", () => {
render(
<Card data-testid="c" size="sm">
x
</Card>,
);
expect(screen.getByTestId("c")).toHaveAttribute("data-size", "sm");
});
it("merges custom className", () => {
render(
<Card data-testid="c" className="my-custom">
x
</Card>,
);
expect(screen.getByTestId("c")).toHaveClass("my-custom");
});
});
describe("Card subcomponents", () => {
it.each([
["CardHeader", CardHeader, "card-header"],
["CardTitle", CardTitle, "card-title"],
["CardDescription", CardDescription, "card-description"],
["CardAction", CardAction, "card-action"],
["CardContent", CardContent, "card-content"],
["CardFooter", CardFooter, "card-footer"],
] as const)("%s renders with data-slot='%s'", (_name, Component, slot) => {
render(
<Component data-testid="el">
<span>child</span>
</Component>,
);
expect(screen.getByTestId("el")).toHaveAttribute("data-slot", slot);
});
it("CardHeader merges custom className", () => {
render(
<CardHeader data-testid="h" className="my-class">
x
</CardHeader>,
);
expect(screen.getByTestId("h")).toHaveClass("my-class");
});
it("CardTitle merges custom className", () => {
render(
<CardTitle data-testid="t" className="my-class">
x
</CardTitle>,
);
expect(screen.getByTestId("t")).toHaveClass("my-class");
});
it("CardFooter merges custom className", () => {
render(
<CardFooter data-testid="f" className="my-class">
x
</CardFooter>,
);
expect(screen.getByTestId("f")).toHaveClass("my-class");
});
});
describe("Card composition", () => {
it("composes a full card", () => {
render(
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
<CardAction>Action</CardAction>
</CardHeader>
<CardContent>Content</CardContent>
<CardFooter>Footer</CardFooter>
</Card>,
);
expect(screen.getByText("Title")).toBeInTheDocument();
expect(screen.getByText("Description")).toBeInTheDocument();
expect(screen.getByText("Action")).toBeInTheDocument();
expect(screen.getByText("Content")).toBeInTheDocument();
expect(screen.getByText("Footer")).toBeInTheDocument();
});
});
@@ -0,0 +1,47 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Checkbox } from "./checkbox";
describe("Checkbox", () => {
it("renders with data-slot='checkbox'", () => {
render(<Checkbox data-testid="cb" />);
expect(screen.getByTestId("cb")).toHaveAttribute("data-slot", "checkbox");
});
it("defaults to unchecked", () => {
render(<Checkbox data-testid="cb" />);
const cb = screen.getByTestId("cb");
// base-ui sets aria-checked
expect(cb.getAttribute("aria-checked")).toBe("false");
});
it("renders as checked via defaultChecked", () => {
render(<Checkbox data-testid="cb" defaultChecked />);
expect(screen.getByTestId("cb").getAttribute("aria-checked")).toBe("true");
});
it("can be controlled via checked prop", () => {
render(<Checkbox data-testid="cb" checked onCheckedChange={() => {}} />);
expect(screen.getByTestId("cb").getAttribute("aria-checked")).toBe("true");
});
it("calls onCheckedChange when toggled", async () => {
const onChange = vi.fn();
render(<Checkbox data-testid="cb" onCheckedChange={onChange} />);
await userEvent.click(screen.getByTestId("cb"));
expect(onChange).toHaveBeenCalledOnce();
});
it("respects disabled state via aria-disabled (base-ui uses span+role)", () => {
render(<Checkbox data-testid="cb" disabled />);
const cb = screen.getByTestId("cb");
expect(cb.getAttribute("aria-disabled")).toBe("true");
expect(cb).toHaveAttribute("data-disabled");
});
it("merges custom className", () => {
render(<Checkbox data-testid="cb" className="my-cb" />);
expect(screen.getByTestId("cb")).toHaveClass("my-cb");
});
});
@@ -0,0 +1,223 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import {
ComboboxChips,
ComboboxChipsInput,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxRoot,
ComboboxSeparator,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
useFilter,
} from "./combobox";
const items = ["Apple", "Banana", "Cherry"];
const renderCombobox = (overrides: Partial<Parameters<typeof ComboboxRoot>[0]> = {}) =>
render(
<ComboboxRoot items={items} {...overrides}>
<ComboboxInput placeholder="Search" />
<ComboboxContent>
<ComboboxList>
{items.map((item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</ComboboxRoot>,
);
describe("ComboboxRoot/Input", () => {
it("renders ComboboxInput with data-slot='combobox-input' (input is from primitive)", () => {
const { container } = renderCombobox();
// The InputGroupInput wraps with data-slot 'input-group-control' which is from input-group
expect(container.querySelector("[data-slot=input-group-control]")).toBeInTheDocument();
});
it("does not render content when closed", () => {
renderCombobox();
expect(screen.queryByText("Apple")).not.toBeInTheDocument();
});
it("renders content when open=true", () => {
renderCombobox({ open: true });
expect(screen.getByText("Apple")).toBeInTheDocument();
expect(screen.getByText("Banana")).toBeInTheDocument();
});
});
describe("ComboboxValue", () => {
it("renders inside primitive without throwing", () => {
expect(() =>
render(
<ComboboxRoot items={items} value="Apple">
<ComboboxValue />
</ComboboxRoot>,
),
).not.toThrow();
});
});
describe("ComboboxItem", () => {
it("renders with data-slot='combobox-item'", () => {
renderCombobox({ open: true });
expect(document.querySelectorAll("[data-slot=combobox-item]").length).toBeGreaterThan(0);
});
});
describe("ComboboxList / ComboboxGroup / ComboboxLabel", () => {
it("ComboboxList renders with data-slot='combobox-list'", () => {
renderCombobox({ open: true });
expect(document.querySelector("[data-slot=combobox-list]")).toBeInTheDocument();
});
it("ComboboxGroup and ComboboxLabel render with their slots", () => {
render(
<ComboboxRoot items={items} open>
<ComboboxInput />
<ComboboxContent>
<ComboboxList>
<ComboboxGroup items={items}>
<ComboboxLabel>Fruits</ComboboxLabel>
<ComboboxItem value="Apple">Apple</ComboboxItem>
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</ComboboxRoot>,
);
expect(document.querySelector("[data-slot=combobox-group]")).toBeInTheDocument();
expect(document.querySelector("[data-slot=combobox-label]")).toBeInTheDocument();
});
});
describe("ComboboxSeparator and ComboboxEmpty", () => {
it("ComboboxSeparator renders with data-slot='combobox-separator'", () => {
render(
<ComboboxRoot items={items} open>
<ComboboxInput />
<ComboboxContent>
<ComboboxList>
<ComboboxItem value="Apple">Apple</ComboboxItem>
<ComboboxSeparator />
<ComboboxItem value="Banana">Banana</ComboboxItem>
</ComboboxList>
</ComboboxContent>
</ComboboxRoot>,
);
expect(document.querySelector("[data-slot=combobox-separator]")).toBeInTheDocument();
});
it("ComboboxEmpty renders with data-slot='combobox-empty'", () => {
render(
<ComboboxRoot items={[]} open>
<ComboboxInput />
<ComboboxContent>
<ComboboxEmpty>No results</ComboboxEmpty>
</ComboboxContent>
</ComboboxRoot>,
);
expect(document.querySelector("[data-slot=combobox-empty]")).toBeInTheDocument();
});
});
describe("ComboboxCollection", () => {
it("renders without throwing", () => {
expect(() =>
render(
<ComboboxRoot items={items} open>
<ComboboxInput />
<ComboboxContent>
<ComboboxList>
<ComboboxCollection items={items}>
{(item) => <ComboboxItem value={item}>{item}</ComboboxItem>}
</ComboboxCollection>
</ComboboxList>
</ComboboxContent>
</ComboboxRoot>,
),
).not.toThrow();
});
});
describe("ComboboxClear", () => {
it("renders inside ComboboxInput when showClear=true", () => {
const { container } = render(
<ComboboxRoot items={items} value="Apple">
<ComboboxInput showClear />
</ComboboxRoot>,
);
expect(container.querySelector("[data-slot=combobox-clear]")).toBeInTheDocument();
});
});
describe("ComboboxTrigger", () => {
it("renders an input-group-button addon when showTrigger=true (default)", () => {
const { container } = renderCombobox();
// ComboboxTrigger is rendered inside InputGroupButton which sets its own data-slot.
expect(container.querySelector("[data-slot=input-group-button]")).toBeInTheDocument();
});
it("hides input-group-button addon when showTrigger=false and showClear=false", () => {
const { container } = render(
<ComboboxRoot items={items}>
<ComboboxInput showTrigger={false} />
</ComboboxRoot>,
);
expect(container.querySelector("[data-slot=input-group-button]")).not.toBeInTheDocument();
});
it("ComboboxTrigger renders standalone with data-slot='combobox-trigger'", () => {
const { container } = render(
<ComboboxRoot items={items}>
<ComboboxTrigger>Open</ComboboxTrigger>
</ComboboxRoot>,
);
expect(container.querySelector("[data-slot=combobox-trigger]")).toBeInTheDocument();
});
});
describe("ComboboxChips and friends", () => {
it("renders ComboboxChips with data-slot='combobox-chips'", () => {
const { container } = render(
<ComboboxRoot items={items} multiple value={["Apple"]}>
<ComboboxChips>
<ComboboxChipsInput />
</ComboboxChips>
</ComboboxRoot>,
);
expect(container.querySelector("[data-slot=combobox-chips]")).toBeInTheDocument();
});
it("renders ComboboxChipsInput with data-slot='combobox-chip-input'", () => {
const { container } = render(
<ComboboxRoot items={items} multiple value={["Apple"]}>
<ComboboxChips>
<ComboboxChipsInput />
</ComboboxChips>
</ComboboxRoot>,
);
expect(container.querySelector("[data-slot=combobox-chip-input]")).toBeInTheDocument();
});
});
describe("useFilter", () => {
it("is exported as a function", () => {
expect(typeof useFilter).toBe("function");
});
});
describe("useComboboxAnchor", () => {
it("is exported as a function", () => {
expect(typeof useComboboxAnchor).toBe("function");
});
});
+129
View File
@@ -0,0 +1,129 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "./command";
describe("Command", () => {
it("renders with data-slot='command'", () => {
const { container } = render(
<Command>
<CommandList>
<CommandItem>x</CommandItem>
</CommandList>
</Command>,
);
expect(container.querySelector("[data-slot=command]")).toBeInTheDocument();
});
it("merges custom className on root", () => {
const { container } = render(
<Command className="my-cmd">
<CommandList />
</Command>,
);
expect(container.querySelector("[data-slot=command]")).toHaveClass("my-cmd");
});
});
describe("CommandInput", () => {
it("renders with data-slot='command-input'", () => {
const { container } = render(
<Command>
<CommandInput placeholder="Search..." />
</Command>,
);
expect(container.querySelector("[data-slot=command-input]")).toBeInTheDocument();
});
it("wrapper uses data-slot='command-input-wrapper'", () => {
const { container } = render(
<Command>
<CommandInput />
</Command>,
);
expect(container.querySelector("[data-slot=command-input-wrapper]")).toBeInTheDocument();
});
});
describe("CommandList / CommandGroup", () => {
it("CommandList uses data-slot='command-list'", () => {
const { container } = render(
<Command>
<CommandList>x</CommandList>
</Command>,
);
expect(container.querySelector("[data-slot=command-list]")).toBeInTheDocument();
});
it("CommandEmpty renders without throwing in tree", () => {
// CommandEmpty only mounts under specific cmdk states — just verify no crash
expect(() =>
render(
<Command>
<CommandList>
<CommandEmpty>No results</CommandEmpty>
</CommandList>
</Command>,
),
).not.toThrow();
});
it("CommandGroup uses data-slot='command-group'", () => {
const { container } = render(
<Command>
<CommandList>
<CommandGroup heading="Group 1">
<CommandItem>Item</CommandItem>
</CommandGroup>
</CommandList>
</Command>,
);
expect(container.querySelector("[data-slot=command-group]")).toBeInTheDocument();
});
});
describe("CommandItem", () => {
it("uses data-slot='command-item'", () => {
const { container } = render(
<Command>
<CommandList>
<CommandItem>Hello</CommandItem>
</CommandList>
</Command>,
);
expect(container.querySelector("[data-slot=command-item]")).toBeInTheDocument();
});
});
describe("CommandSeparator", () => {
it("uses data-slot='command-separator'", () => {
const { container } = render(
<Command>
<CommandList>
<CommandSeparator />
</CommandList>
</Command>,
);
expect(container.querySelector("[data-slot=command-separator]")).toBeInTheDocument();
});
});
describe("CommandShortcut", () => {
it("uses data-slot='command-shortcut'", () => {
render(<CommandShortcut>K</CommandShortcut>);
expect(screen.getByText("⌘K")).toHaveAttribute("data-slot", "command-shortcut");
});
it("merges custom className", () => {
render(<CommandShortcut className="my-class">x</CommandShortcut>);
expect(screen.getByText("x")).toHaveClass("my-class");
});
});
@@ -0,0 +1,133 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "./context-menu";
const renderWithMenu = (children: React.ReactNode) =>
render(
<ContextMenu open>
<ContextMenuTrigger>x</ContextMenuTrigger>
<ContextMenuContent>{children}</ContextMenuContent>
</ContextMenu>,
);
describe("ContextMenu", () => {
it("trigger uses data-slot='context-menu-trigger'", () => {
render(
<ContextMenu>
<ContextMenuTrigger data-testid="trigger">Right-click me</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>x</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>,
);
expect(screen.getByTestId("trigger")).toHaveAttribute("data-slot", "context-menu-trigger");
});
it("does not render content by default", () => {
render(
<ContextMenu>
<ContextMenuTrigger>x</ContextMenuTrigger>
<ContextMenuContent>Hidden</ContextMenuContent>
</ContextMenu>,
);
expect(screen.queryByText("Hidden")).not.toBeInTheDocument();
});
});
describe("ContextMenuItem", () => {
it("uses data-slot='context-menu-item'", () => {
renderWithMenu(<ContextMenuItem>Item</ContextMenuItem>);
expect(screen.getByText("Item")).toHaveAttribute("data-slot", "context-menu-item");
});
it("defaults variant='default'", () => {
renderWithMenu(<ContextMenuItem>Item</ContextMenuItem>);
expect(screen.getByText("Item")).toHaveAttribute("data-variant", "default");
});
it("supports variant='destructive'", () => {
renderWithMenu(<ContextMenuItem variant="destructive">Delete</ContextMenuItem>);
expect(screen.getByText("Delete")).toHaveAttribute("data-variant", "destructive");
});
it("supports inset prop", () => {
renderWithMenu(<ContextMenuItem inset>Inset</ContextMenuItem>);
expect(screen.getByText("Inset")).toHaveAttribute("data-inset", "true");
});
});
describe("ContextMenuLabel", () => {
it("uses data-slot='context-menu-label'", () => {
renderWithMenu(
<ContextMenuGroup>
<ContextMenuLabel>Group label</ContextMenuLabel>
</ContextMenuGroup>,
);
expect(screen.getByText("Group label")).toHaveAttribute("data-slot", "context-menu-label");
});
it("supports inset prop", () => {
renderWithMenu(
<ContextMenuGroup>
<ContextMenuLabel inset>Inset label</ContextMenuLabel>
</ContextMenuGroup>,
);
expect(screen.getByText("Inset label")).toHaveAttribute("data-inset", "true");
});
});
describe("ContextMenuShortcut", () => {
it("uses data-slot='context-menu-shortcut'", () => {
render(<ContextMenuShortcut>K</ContextMenuShortcut>);
expect(screen.getByText("⌘K")).toHaveAttribute("data-slot", "context-menu-shortcut");
});
it("merges custom className", () => {
render(<ContextMenuShortcut className="my-class">x</ContextMenuShortcut>);
expect(screen.getByText("x")).toHaveClass("my-class");
});
});
describe("ContextMenuCheckboxItem and ContextMenuRadioItem", () => {
it("CheckboxItem renders within menu", () => {
renderWithMenu(<ContextMenuCheckboxItem checked>Toggle</ContextMenuCheckboxItem>);
expect(screen.getByText("Toggle")).toHaveAttribute("data-slot", "context-menu-checkbox-item");
});
it("RadioItem renders within menu", () => {
renderWithMenu(
<ContextMenuRadioGroup value="a">
<ContextMenuRadioItem value="a">A</ContextMenuRadioItem>
</ContextMenuRadioGroup>,
);
expect(screen.getByText("A")).toHaveAttribute("data-slot", "context-menu-radio-item");
});
});
describe("ContextMenuSub", () => {
it("SubTrigger uses data-slot='context-menu-sub-trigger'", () => {
renderWithMenu(
<ContextMenuSub>
<ContextMenuSubTrigger>More</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>Inner</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>,
);
expect(screen.getByText("More")).toHaveAttribute("data-slot", "context-menu-sub-trigger");
});
});
+135
View File
@@ -0,0 +1,135 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./dialog";
describe("Dialog", () => {
it("does not render content by default", () => {
render(
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>Hidden body</DialogContent>
</Dialog>,
);
expect(screen.queryByText("Hidden body")).not.toBeInTheDocument();
});
it("opens content via defaultOpen", () => {
render(
<Dialog defaultOpen>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>Visible body</DialogContent>
</Dialog>,
);
expect(screen.getByText("Visible body")).toBeInTheDocument();
});
it("opens content via controlled open", () => {
render(
<Dialog open>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>Visible body</DialogContent>
</Dialog>,
);
expect(screen.getByText("Visible body")).toBeInTheDocument();
});
it("opens on trigger click", async () => {
render(
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>Body text</DialogContent>
</Dialog>,
);
await userEvent.click(screen.getByText("Open"));
expect(await screen.findByText("Body text")).toBeInTheDocument();
});
it("renders close button by default in DialogContent", () => {
render(
<Dialog open>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>body</DialogContent>
</Dialog>,
);
expect(screen.getByText("Close")).toBeInTheDocument();
});
it("hides close button when showCloseButton=false", () => {
render(
<Dialog open>
<DialogTrigger>Open</DialogTrigger>
<DialogContent showCloseButton={false}>body</DialogContent>
</Dialog>,
);
expect(screen.queryByText("Close")).not.toBeInTheDocument();
});
});
describe("DialogTitle", () => {
it("uses data-slot='dialog-title'", () => {
render(
<Dialog open>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogTitle>My Title</DialogTitle>
</DialogContent>
</Dialog>,
);
expect(screen.getByText("My Title")).toHaveAttribute("data-slot", "dialog-title");
});
});
describe("DialogDescription", () => {
it("uses data-slot='dialog-description'", () => {
render(
<Dialog open>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogDescription>My Description</DialogDescription>
</DialogContent>
</Dialog>,
);
expect(screen.getByText("My Description")).toHaveAttribute("data-slot", "dialog-description");
});
});
describe("DialogHeader", () => {
it("uses data-slot='dialog-header'", () => {
render(<DialogHeader data-testid="h">x</DialogHeader>);
expect(screen.getByTestId("h")).toHaveAttribute("data-slot", "dialog-header");
});
});
describe("DialogFooter", () => {
it("uses data-slot='dialog-footer'", () => {
render(<DialogFooter data-testid="f">x</DialogFooter>);
expect(screen.getByTestId("f")).toHaveAttribute("data-slot", "dialog-footer");
});
it("renders close button when showCloseButton=true", () => {
render(
<Dialog open>
<DialogTrigger>x</DialogTrigger>
<DialogContent showCloseButton={false}>
<DialogFooter showCloseButton>body</DialogFooter>
</DialogContent>
</Dialog>,
);
expect(screen.getByText("Close")).toBeInTheDocument();
});
it("does not render close button by default", () => {
render(<DialogFooter>just content</DialogFooter>);
expect(screen.queryByText("Close")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,197 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "./dropdown-menu";
describe("DropdownMenu", () => {
it("trigger has data-slot='dropdown-menu-trigger'", () => {
render(
<DropdownMenu>
<DropdownMenuTrigger data-testid="trigger">Open</DropdownMenuTrigger>
<DropdownMenuContent>x</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByTestId("trigger")).toHaveAttribute("data-slot", "dropdown-menu-trigger");
});
it("does not render content by default", () => {
render(
<DropdownMenu>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent>Hidden item</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.queryByText("Hidden item")).not.toBeInTheDocument();
});
it("renders content when controlled open", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item 1</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByText("Item 1")).toBeInTheDocument();
});
});
describe("DropdownMenuItem", () => {
it("uses data-slot='dropdown-menu-item'", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>x</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByText("Item")).toHaveAttribute("data-slot", "dropdown-menu-item");
});
it("defaults variant='default'", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>x</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByText("Item")).toHaveAttribute("data-variant", "default");
});
it("supports variant='destructive'", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>x</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByText("Delete")).toHaveAttribute("data-variant", "destructive");
});
it("supports inset prop", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>x</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem inset>Inset</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByText("Inset")).toHaveAttribute("data-inset", "true");
});
});
describe("DropdownMenuLabel", () => {
it("uses data-slot='dropdown-menu-label'", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>x</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuLabel>Group label</DropdownMenuLabel>
<DropdownMenuItem>Item</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByText("Group label")).toHaveAttribute("data-slot", "dropdown-menu-label");
});
});
describe("DropdownMenuSeparator", () => {
it("uses data-slot='dropdown-menu-separator'", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>x</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>1</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>2</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>,
);
// Portal content renders at document.body
expect(document.querySelector("[data-slot=dropdown-menu-separator]")).toBeInTheDocument();
});
});
describe("DropdownMenuShortcut", () => {
it("uses data-slot='dropdown-menu-shortcut'", () => {
render(<DropdownMenuShortcut>K</DropdownMenuShortcut>);
expect(screen.getByText("⌘K")).toHaveAttribute("data-slot", "dropdown-menu-shortcut");
});
it("merges custom className", () => {
render(<DropdownMenuShortcut className="my-class">x</DropdownMenuShortcut>);
expect(screen.getByText("x")).toHaveClass("my-class");
});
});
describe("DropdownMenuCheckboxItem", () => {
it("uses data-slot='dropdown-menu-checkbox-item'", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>x</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuCheckboxItem checked>Toggle</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByText("Toggle")).toHaveAttribute("data-slot", "dropdown-menu-checkbox-item");
});
});
describe("DropdownMenuRadioItem", () => {
it("uses data-slot='dropdown-menu-radio-item'", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>x</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup value="a">
<DropdownMenuRadioItem value="a">A</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByText("A")).toHaveAttribute("data-slot", "dropdown-menu-radio-item");
});
});
describe("DropdownMenuSub", () => {
it("renders nested submenu trigger", () => {
render(
<DropdownMenu open>
<DropdownMenuTrigger>x</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuSub>
<DropdownMenuSubTrigger>More</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Inner</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>,
);
expect(screen.getByText("More")).toHaveAttribute("data-slot", "dropdown-menu-sub-trigger");
});
});
+185
View File
@@ -0,0 +1,185 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "./form";
describe("FormItem", () => {
it("renders with data-slot='form-item'", () => {
render(<FormItem data-testid="i">x</FormItem>);
expect(screen.getByTestId("i")).toHaveAttribute("data-slot", "form-item");
});
it("merges custom className", () => {
render(
<FormItem data-testid="i" className="my-class">
x
</FormItem>,
);
expect(screen.getByTestId("i")).toHaveClass("my-class");
});
it("provides id and hasError context to children", () => {
render(
<FormItem hasError>
<FormLabel>Name</FormLabel>
<FormControl render={(props) => <input {...props} data-testid="input" />} />
</FormItem>,
);
const label = screen.getByText("Name");
expect(label).toHaveAttribute("data-error", "true");
const input = screen.getByTestId("input");
expect(input).toHaveAttribute("aria-invalid", "true");
});
it("hasError defaults to false", () => {
render(
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl render={(props) => <input {...props} data-testid="input" />} />
</FormItem>,
);
const input = screen.getByTestId("input");
expect(input).toHaveAttribute("aria-invalid", "false");
});
});
describe("FormLabel", () => {
it("renders with data-slot='form-label' and connects htmlFor to control via context id", () => {
render(
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl render={(props) => <input {...props} type="email" />} />
</FormItem>,
);
const label = screen.getByText("Email");
expect(label).toHaveAttribute("data-slot", "form-label");
const htmlFor = label.getAttribute("for");
expect(htmlFor).toMatch(/-form-item$/);
});
});
describe("FormControl", () => {
it("sets aria-describedby pointing only to description when no error", () => {
render(
<FormItem>
<FormControl render={(props) => <input {...props} data-testid="input" />} />
<FormDescription>Help text</FormDescription>
</FormItem>,
);
const input = screen.getByTestId("input");
const describedBy = input.getAttribute("aria-describedby");
expect(describedBy).toMatch(/-form-item-description$/);
expect(describedBy).not.toContain("message");
});
it("includes message in aria-describedby when hasError=true", () => {
render(
<FormItem hasError>
<FormControl render={(props) => <input {...props} data-testid="input" />} />
</FormItem>,
);
const input = screen.getByTestId("input");
const describedBy = input.getAttribute("aria-describedby");
expect(describedBy).toContain("description");
expect(describedBy).toContain("message");
});
});
describe("FormDescription", () => {
it("uses data-slot='form-description'", () => {
render(
<FormItem>
<FormDescription>Help</FormDescription>
</FormItem>,
);
expect(screen.getByText("Help")).toHaveAttribute("data-slot", "form-description");
});
it("id ends with '-form-item-description'", () => {
render(
<FormItem>
<FormDescription>Help</FormDescription>
</FormItem>,
);
expect(screen.getByText("Help").getAttribute("id")).toMatch(/-form-item-description$/);
});
});
describe("FormMessage", () => {
it("returns null when no errors", () => {
const { container } = render(
<FormItem>
<FormMessage />
</FormItem>,
);
expect(container.querySelector("[data-slot=form-message]")).not.toBeInTheDocument();
});
it("returns null when errors is empty", () => {
const { container } = render(
<FormItem>
<FormMessage errors={[]} />
</FormItem>,
);
expect(container.querySelector("[data-slot=form-message]")).not.toBeInTheDocument();
});
it("renders string error message", () => {
render(
<FormItem hasError>
<FormMessage errors={["String error"]} />
</FormItem>,
);
expect(screen.getByText("String error")).toBeInTheDocument();
});
it("renders error with .message property", () => {
render(
<FormItem hasError>
<FormMessage errors={[{ message: "Object error" }]} />
</FormItem>,
);
expect(screen.getByText("Object error")).toBeInTheDocument();
});
it("skips falsy and unrecognized errors and shows the first valid one", () => {
render(
<FormItem hasError>
<FormMessage errors={[null, undefined, { wrong: "field" }, "Valid"]} />
</FormItem>,
);
expect(screen.getByText("Valid")).toBeInTheDocument();
});
it("returns null when no extractable error message in array", () => {
const { container } = render(
<FormItem>
<FormMessage errors={[null, undefined, { wrong: "field" }]} />
</FormItem>,
);
expect(container.querySelector("[data-slot=form-message]")).not.toBeInTheDocument();
});
it("uses destructive class when hasError=true", () => {
render(
<FormItem hasError>
<FormMessage errors={["Bad"]} />
</FormItem>,
);
expect(screen.getByText("Bad")).toHaveClass("text-destructive");
});
it("uses muted class when hasError=false but errors are passed", () => {
render(
<FormItem>
<FormMessage errors={["Hint"]} />
</FormItem>,
);
expect(screen.getByText("Hint")).toHaveClass("text-muted-foreground");
});
});
@@ -0,0 +1,55 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card";
describe("HoverCard", () => {
it("trigger has data-slot='hover-card-trigger'", () => {
render(
<HoverCard>
<HoverCardTrigger data-testid="trigger">Hover</HoverCardTrigger>
<HoverCardContent>preview</HoverCardContent>
</HoverCard>,
);
expect(screen.getByTestId("trigger")).toHaveAttribute("data-slot", "hover-card-trigger");
});
it("does not render content by default", () => {
render(
<HoverCard>
<HoverCardTrigger>Hover</HoverCardTrigger>
<HoverCardContent>Hidden preview</HoverCardContent>
</HoverCard>,
);
expect(screen.queryByText("Hidden preview")).not.toBeInTheDocument();
});
it("renders content when controlled open", () => {
render(
<HoverCard open>
<HoverCardTrigger>Hover</HoverCardTrigger>
<HoverCardContent>Visible preview</HoverCardContent>
</HoverCard>,
);
expect(screen.getByText("Visible preview")).toBeInTheDocument();
});
it("HoverCardContent uses data-slot='hover-card-content' when shown", () => {
render(
<HoverCard open>
<HoverCardTrigger>x</HoverCardTrigger>
<HoverCardContent>visible</HoverCardContent>
</HoverCard>,
);
expect(screen.getByText("visible")).toHaveAttribute("data-slot", "hover-card-content");
});
it("merges custom className on HoverCardContent", () => {
render(
<HoverCard open>
<HoverCardTrigger>x</HoverCardTrigger>
<HoverCardContent className="my-class">visible</HoverCardContent>
</HoverCard>,
);
expect(screen.getByText("visible")).toHaveClass("my-class");
});
});
@@ -0,0 +1,147 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "./input-group";
describe("InputGroup", () => {
it("renders a fieldset with data-slot='input-group'", () => {
render(<InputGroup data-testid="g" />);
const g = screen.getByTestId("g");
expect(g.tagName).toBe("FIELDSET");
expect(g).toHaveAttribute("data-slot", "input-group");
});
it("merges custom className", () => {
render(<InputGroup data-testid="g" className="my-class" />);
expect(screen.getByTestId("g")).toHaveClass("my-class");
});
});
describe("InputGroupAddon", () => {
it("defaults align to 'inline-start'", () => {
render(<InputGroupAddon data-testid="addon" />);
expect(screen.getByTestId("addon")).toHaveAttribute("data-align", "inline-start");
});
it.each(["inline-start", "inline-end", "block-start", "block-end"] as const)("supports align=%s", (align) => {
render(<InputGroupAddon data-testid="addon" align={align} />);
expect(screen.getByTestId("addon")).toHaveAttribute("data-align", align);
});
it("uses data-slot='input-group-addon'", () => {
render(<InputGroupAddon data-testid="addon" />);
expect(screen.getByTestId("addon")).toHaveAttribute("data-slot", "input-group-addon");
});
it("focuses sibling input on click outside of buttons", () => {
render(
<InputGroup>
<InputGroupAddon data-testid="addon">addon</InputGroupAddon>
<InputGroupInput data-testid="i" />
</InputGroup>,
);
const addon = screen.getByTestId("addon");
fireEvent.click(addon);
expect(document.activeElement).toBe(screen.getByTestId("i"));
});
it("focuses sibling input on Enter key", () => {
render(
<InputGroup>
<InputGroupAddon data-testid="addon">addon</InputGroupAddon>
<InputGroupInput data-testid="i" />
</InputGroup>,
);
const addon = screen.getByTestId("addon");
fireEvent.keyDown(addon, { key: "Enter" });
expect(document.activeElement).toBe(screen.getByTestId("i"));
});
it("focuses sibling input on Space key", () => {
render(
<InputGroup>
<InputGroupAddon data-testid="addon">addon</InputGroupAddon>
<InputGroupInput data-testid="i" />
</InputGroup>,
);
const addon = screen.getByTestId("addon");
fireEvent.keyDown(addon, { key: " " });
expect(document.activeElement).toBe(screen.getByTestId("i"));
});
it("ignores unrelated keys", () => {
render(
<InputGroup>
<InputGroupAddon data-testid="addon">addon</InputGroupAddon>
<InputGroupInput data-testid="i" />
</InputGroup>,
);
const addon = screen.getByTestId("addon");
fireEvent.keyDown(addon, { key: "a" });
expect(document.activeElement).not.toBe(screen.getByTestId("i"));
});
});
describe("InputGroupButton", () => {
it("defaults to type='button' and variant='ghost'", () => {
render(<InputGroupButton data-testid="b">x</InputGroupButton>);
const button = screen.getByTestId("b");
expect(button).toHaveAttribute("type", "button");
});
it("respects type override", () => {
render(
<InputGroupButton data-testid="b" type="submit">
x
</InputGroupButton>,
);
expect(screen.getByTestId("b")).toHaveAttribute("type", "submit");
});
it("applies data-size for size variants", () => {
render(
<InputGroupButton data-testid="b" size="sm">
x
</InputGroupButton>,
);
expect(screen.getByTestId("b")).toHaveAttribute("data-size", "sm");
});
});
describe("InputGroupText", () => {
it("renders as a span", () => {
render(<InputGroupText>label</InputGroupText>);
expect(screen.getByText("label").tagName).toBe("SPAN");
});
it("merges custom className", () => {
render(<InputGroupText className="my-class">x</InputGroupText>);
expect(screen.getByText("x")).toHaveClass("my-class");
});
});
describe("InputGroupInput", () => {
it("uses data-slot='input-group-control'", () => {
render(<InputGroupInput data-testid="i" />);
expect(screen.getByTestId("i")).toHaveAttribute("data-slot", "input-group-control");
});
});
describe("InputGroupTextarea", () => {
it("uses data-slot='input-group-control' and renders textarea", () => {
render(<InputGroupTextarea data-testid="t" />);
const t = screen.getByTestId("t");
expect(t.tagName).toBe("TEXTAREA");
expect(t).toHaveAttribute("data-slot", "input-group-control");
});
});
+49
View File
@@ -0,0 +1,49 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Input } from "./input";
describe("Input", () => {
it("renders an input element", () => {
render(<Input data-testid="x" />);
expect(screen.getByTestId("x").tagName).toBe("INPUT");
});
it("applies data-slot='input'", () => {
render(<Input data-testid="x" />);
expect(screen.getByTestId("x")).toHaveAttribute("data-slot", "input");
});
it("merges custom className", () => {
render(<Input data-testid="x" className="custom-input" />);
expect(screen.getByTestId("x")).toHaveClass("custom-input");
});
it("forwards type attribute", () => {
render(<Input data-testid="x" type="email" />);
expect(screen.getByTestId("x")).toHaveAttribute("type", "email");
});
it("calls onChange when typing", async () => {
const onChange = vi.fn();
render(<Input data-testid="x" onChange={onChange} />);
await userEvent.type(screen.getByTestId("x"), "abc");
expect(onChange).toHaveBeenCalled();
});
it("forwards placeholder", () => {
render(<Input placeholder="Enter name" />);
expect(screen.getByPlaceholderText("Enter name")).toBeInTheDocument();
});
it("respects disabled state", async () => {
render(<Input data-testid="x" disabled />);
const input = screen.getByTestId("x");
expect(input).toBeDisabled();
});
it("supports controlled value", () => {
render(<Input data-testid="x" value="hello" onChange={() => {}} />);
expect(screen.getByTestId("x")).toHaveValue("hello");
});
});
+55
View File
@@ -0,0 +1,55 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Kbd, KbdGroup } from "./kbd";
describe("Kbd", () => {
it("renders as a <kbd> element with data-slot='kbd'", () => {
render(<Kbd data-testid="k">Ctrl</Kbd>);
const k = screen.getByTestId("k");
expect(k.tagName).toBe("KBD");
expect(k).toHaveAttribute("data-slot", "kbd");
});
it("renders the key text", () => {
render(<Kbd> K</Kbd>);
expect(screen.getByText("⌘ K")).toBeInTheDocument();
});
it("merges custom className", () => {
render(
<Kbd data-testid="k" className="my-class">
x
</Kbd>,
);
expect(screen.getByTestId("k")).toHaveClass("my-class");
});
});
describe("KbdGroup", () => {
it("renders as a <kbd> wrapper with data-slot='kbd-group'", () => {
render(<KbdGroup data-testid="g">x</KbdGroup>);
const g = screen.getByTestId("g");
expect(g.tagName).toBe("KBD");
expect(g).toHaveAttribute("data-slot", "kbd-group");
});
it("groups multiple Kbd elements", () => {
render(
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>K</Kbd>
</KbdGroup>,
);
expect(screen.getByText("Ctrl")).toBeInTheDocument();
expect(screen.getByText("K")).toBeInTheDocument();
});
it("merges custom className", () => {
render(
<KbdGroup data-testid="g" className="my-group">
x
</KbdGroup>,
);
expect(screen.getByTestId("g")).toHaveClass("my-group");
});
});
+31
View File
@@ -0,0 +1,31 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Label } from "./label";
describe("Label", () => {
it("renders as a <label> element", () => {
render(<Label>Name</Label>);
expect(screen.getByText("Name").tagName).toBe("LABEL");
});
it("applies data-slot='label'", () => {
render(<Label>x</Label>);
expect(screen.getByText("x")).toHaveAttribute("data-slot", "label");
});
it("merges custom className", () => {
render(<Label className="my-custom">x</Label>);
expect(screen.getByText("x")).toHaveClass("my-custom");
});
it("supports htmlFor association with inputs", () => {
render(
<>
<Label htmlFor="name">Name</Label>
<input id="name" />
</>,
);
const input = screen.getByLabelText("Name");
expect(input.tagName).toBe("INPUT");
});
});
+116
View File
@@ -0,0 +1,116 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "./popover";
describe("Popover", () => {
it("trigger element has data-slot='popover-trigger'", () => {
render(
<Popover>
<PopoverTrigger data-testid="trigger">Open</PopoverTrigger>
<PopoverContent>content</PopoverContent>
</Popover>,
);
expect(screen.getByTestId("trigger")).toHaveAttribute("data-slot", "popover-trigger");
});
it("opens content on trigger click", async () => {
render(
<Popover>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>Hidden text</PopoverContent>
</Popover>,
);
expect(screen.queryByText("Hidden text")).not.toBeInTheDocument();
await userEvent.click(screen.getByText("Open"));
expect(await screen.findByText("Hidden text")).toBeInTheDocument();
});
it("supports default open via prop", () => {
render(
<Popover defaultOpen>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>Initially visible</PopoverContent>
</Popover>,
);
expect(screen.getByText("Initially visible")).toBeInTheDocument();
});
it("can be controlled via open prop", () => {
render(
<Popover open>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>Controlled</PopoverContent>
</Popover>,
);
expect(screen.getByText("Controlled")).toBeInTheDocument();
});
});
describe("PopoverHeader", () => {
it("uses data-slot='popover-header'", () => {
render(<PopoverHeader data-testid="h">Title</PopoverHeader>);
expect(screen.getByTestId("h")).toHaveAttribute("data-slot", "popover-header");
});
it("merges custom className", () => {
render(
<PopoverHeader data-testid="h" className="my-class">
x
</PopoverHeader>,
);
expect(screen.getByTestId("h")).toHaveClass("my-class");
});
});
describe("PopoverTitle", () => {
it("uses data-slot='popover-title'", () => {
render(
<Popover open>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>
<PopoverTitle>My title</PopoverTitle>
</PopoverContent>
</Popover>,
);
expect(screen.getByText("My title")).toHaveAttribute("data-slot", "popover-title");
});
});
describe("PopoverDescription", () => {
it("uses data-slot='popover-description'", () => {
render(
<Popover open>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>
<PopoverDescription>My description</PopoverDescription>
</PopoverContent>
</Popover>,
);
expect(screen.getByText("My description")).toHaveAttribute("data-slot", "popover-description");
});
});
describe("PopoverContent", () => {
it("uses data-slot='popover-content'", () => {
render(
<Popover open>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>visible content</PopoverContent>
</Popover>,
);
const content = screen.getByText("visible content");
expect(content).toHaveAttribute("data-slot", "popover-content");
});
it("merges custom className", () => {
render(
<Popover open>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent className="my-class">visible</PopoverContent>
</Popover>,
);
expect(screen.getByText("visible")).toHaveClass("my-class");
});
});
@@ -0,0 +1,71 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue } from "./progress";
describe("Progress", () => {
it("renders with data-slot='progress'", () => {
render(<Progress data-testid="p" value={50} />);
expect(screen.getByTestId("p")).toHaveAttribute("data-slot", "progress");
});
it("auto-renders track and indicator children", () => {
const { container } = render(<Progress value={50} data-testid="p" />);
expect(container.querySelector("[data-slot=progress-track]")).toBeInTheDocument();
expect(container.querySelector("[data-slot=progress-indicator]")).toBeInTheDocument();
});
it("forwards value attribute via aria-valuenow", () => {
render(<Progress data-testid="p" value={75} />);
expect(screen.getByTestId("p")).toHaveAttribute("aria-valuenow", "75");
});
it("supports custom children rendered before the track", () => {
render(
<Progress data-testid="p" value={50}>
<ProgressLabel>My Progress</ProgressLabel>
</Progress>,
);
expect(screen.getByText("My Progress")).toBeInTheDocument();
});
});
describe("ProgressLabel", () => {
it("renders with data-slot='progress-label'", () => {
render(
<Progress value={50}>
<ProgressLabel>Label</ProgressLabel>
</Progress>,
);
expect(screen.getByText("Label")).toHaveAttribute("data-slot", "progress-label");
});
});
describe("ProgressValue", () => {
it("renders with data-slot='progress-value'", () => {
render(
<Progress value={50}>
<ProgressValue />
</Progress>,
);
// The value text is rendered by base-ui — just verify the slot exists
const { container } = render(
<Progress value={50}>
<ProgressValue data-testid="v" />
</Progress>,
);
expect(container.querySelector("[data-slot=progress-value]")).toBeInTheDocument();
});
});
describe("ProgressTrack and ProgressIndicator", () => {
it("can be rendered explicitly with custom classes", () => {
const { container } = render(
<Progress value={25}>
<ProgressTrack className="track-custom">
<ProgressIndicator className="ind-custom" />
</ProgressTrack>
</Progress>,
);
expect(container.querySelectorAll("[data-slot=progress-track]")).toHaveLength(2);
});
});
@@ -0,0 +1,64 @@
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { ResizableGroup, ResizablePanel, ResizableSeparator } from "./resizable";
describe("ResizableGroup", () => {
it("renders with data-slot='resizable-panel-group'", () => {
const { container } = render(
<ResizableGroup direction="horizontal">
<ResizablePanel>A</ResizablePanel>
<ResizableSeparator />
<ResizablePanel>B</ResizablePanel>
</ResizableGroup>,
);
expect(container.querySelector("[data-slot=resizable-panel-group]")).toBeInTheDocument();
});
it("merges custom className", () => {
const { container } = render(
<ResizableGroup direction="horizontal" className="my-group">
<ResizablePanel>x</ResizablePanel>
</ResizableGroup>,
);
expect(container.querySelector("[data-slot=resizable-panel-group]")).toHaveClass("my-group");
});
});
describe("ResizablePanel", () => {
it("renders with data-slot='resizable-panel'", () => {
const { container } = render(
<ResizableGroup direction="horizontal">
<ResizablePanel>x</ResizablePanel>
</ResizableGroup>,
);
expect(container.querySelector("[data-slot=resizable-panel]")).toBeInTheDocument();
});
});
describe("ResizableSeparator", () => {
it("renders with data-slot='resizable-handle' and no inner handle by default", () => {
const { container } = render(
<ResizableGroup direction="horizontal">
<ResizablePanel>A</ResizablePanel>
<ResizableSeparator />
<ResizablePanel>B</ResizablePanel>
</ResizableGroup>,
);
const handle = container.querySelector("[data-slot=resizable-handle]");
expect(handle).toBeInTheDocument();
// No inner handle div by default
expect(handle?.children).toHaveLength(0);
});
it("renders inner handle when withHandle=true", () => {
const { container } = render(
<ResizableGroup direction="horizontal">
<ResizablePanel>A</ResizablePanel>
<ResizableSeparator withHandle />
<ResizablePanel>B</ResizablePanel>
</ResizableGroup>,
);
const handle = container.querySelector("[data-slot=resizable-handle]");
expect(handle?.children.length).toBeGreaterThan(0);
});
});
@@ -0,0 +1,42 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { ScrollArea } from "./scroll-area";
describe("ScrollArea", () => {
it("renders with data-slot='scroll-area'", () => {
render(
<ScrollArea data-testid="sa">
<div>content</div>
</ScrollArea>,
);
expect(screen.getByTestId("sa")).toHaveAttribute("data-slot", "scroll-area");
});
it("renders viewport with children", () => {
const { container } = render(
<ScrollArea>
<div>content</div>
</ScrollArea>,
);
expect(container.querySelector("[data-slot=scroll-area-viewport]")).toBeInTheDocument();
expect(screen.getByText("content")).toBeInTheDocument();
});
it("merges custom className on root", () => {
render(
<ScrollArea data-testid="sa" className="my-class">
<div>x</div>
</ScrollArea>,
);
expect(screen.getByTestId("sa")).toHaveClass("my-class");
});
it("preserves the relative positioning class", () => {
render(
<ScrollArea data-testid="sa">
<div>x</div>
</ScrollArea>,
);
expect(screen.getByTestId("sa")).toHaveClass("relative");
});
});
@@ -0,0 +1,25 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Separator } from "./separator";
describe("Separator", () => {
it("renders with data-slot='separator'", () => {
render(<Separator data-testid="sep" />);
expect(screen.getByTestId("sep")).toHaveAttribute("data-slot", "separator");
});
it("defaults to horizontal orientation", () => {
render(<Separator data-testid="sep" />);
expect(screen.getByTestId("sep")).toHaveAttribute("data-orientation", "horizontal");
});
it("supports vertical orientation", () => {
render(<Separator data-testid="sep" orientation="vertical" />);
expect(screen.getByTestId("sep")).toHaveAttribute("data-orientation", "vertical");
});
it("merges custom className", () => {
render(<Separator data-testid="sep" className="my-sep" />);
expect(screen.getByTestId("sep")).toHaveClass("my-sep");
});
});
+104
View File
@@ -0,0 +1,104 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./sheet";
describe("Sheet", () => {
it("does not render content by default", () => {
render(
<Sheet>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>Hidden body</SheetContent>
</Sheet>,
);
expect(screen.queryByText("Hidden body")).not.toBeInTheDocument();
});
it("opens via defaultOpen", () => {
render(
<Sheet defaultOpen>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>Visible body</SheetContent>
</Sheet>,
);
expect(screen.getByText("Visible body")).toBeInTheDocument();
});
it("renders default 'right' side", () => {
render(
<Sheet open>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>visible</SheetContent>
</Sheet>,
);
const content = screen.getByText("visible");
expect(content).toHaveAttribute("data-side", "right");
});
it.each(["top", "right", "bottom", "left"] as const)("supports side=%s", (side) => {
render(
<Sheet open>
<SheetTrigger>x</SheetTrigger>
<SheetContent side={side}>visible {side}</SheetContent>
</Sheet>,
);
expect(screen.getByText(`visible ${side}`)).toHaveAttribute("data-side", side);
});
it("renders close button by default", () => {
render(
<Sheet open>
<SheetTrigger>x</SheetTrigger>
<SheetContent>x</SheetContent>
</Sheet>,
);
expect(screen.getByText("Close")).toBeInTheDocument();
});
it("hides close button with showCloseButton=false", () => {
render(
<Sheet open>
<SheetTrigger>x</SheetTrigger>
<SheetContent showCloseButton={false}>x</SheetContent>
</Sheet>,
);
expect(screen.queryByText("Close")).not.toBeInTheDocument();
});
});
describe("SheetHeader / SheetFooter", () => {
it("SheetHeader uses data-slot='sheet-header'", () => {
render(<SheetHeader data-testid="h">x</SheetHeader>);
expect(screen.getByTestId("h")).toHaveAttribute("data-slot", "sheet-header");
});
it("SheetFooter uses data-slot='sheet-footer'", () => {
render(<SheetFooter data-testid="f">x</SheetFooter>);
expect(screen.getByTestId("f")).toHaveAttribute("data-slot", "sheet-footer");
});
});
describe("SheetTitle / SheetDescription", () => {
it("SheetTitle uses data-slot='sheet-title'", () => {
render(
<Sheet open>
<SheetTrigger>x</SheetTrigger>
<SheetContent>
<SheetTitle>Title</SheetTitle>
</SheetContent>
</Sheet>,
);
expect(screen.getByText("Title")).toHaveAttribute("data-slot", "sheet-title");
});
it("SheetDescription uses data-slot='sheet-description'", () => {
render(
<Sheet open>
<SheetTrigger>x</SheetTrigger>
<SheetContent>
<SheetDescription>Description</SheetDescription>
</SheetContent>
</Sheet>,
);
expect(screen.getByText("Description")).toHaveAttribute("data-slot", "sheet-description");
});
});
+358
View File
@@ -0,0 +1,358 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
} from "./sidebar";
const renderInProvider = (children: React.ReactNode, props: Partial<Parameters<typeof SidebarProvider>[0]> = {}) =>
render(
<SidebarProvider {...props}>
<Sidebar>{children}</Sidebar>
</SidebarProvider>,
);
describe("SidebarProvider", () => {
it("renders children inside a sidebar-wrapper div", () => {
const { container } = render(
<SidebarProvider>
<div data-testid="child">child</div>
</SidebarProvider>,
);
expect(container.querySelector("[data-slot=sidebar-wrapper]")).toBeInTheDocument();
expect(screen.getByTestId("child")).toBeInTheDocument();
});
it("merges custom className on wrapper", () => {
const { container } = render(
<SidebarProvider className="my-wrapper">
<div>x</div>
</SidebarProvider>,
);
expect(container.querySelector("[data-slot=sidebar-wrapper]")).toHaveClass("my-wrapper");
});
it("calls onOpenChange when toggled in controlled mode", async () => {
const onOpenChange = vi.fn();
render(
<SidebarProvider open={true} onOpenChange={onOpenChange}>
<Sidebar>x</Sidebar>
<SidebarTrigger />
</SidebarProvider>,
);
await userEvent.click(screen.getByRole("button", { name: /toggle sidebar/i }));
expect(onOpenChange).toHaveBeenCalledWith(false);
});
it("toggles via Cmd/Ctrl+B keyboard shortcut", async () => {
const onOpenChange = vi.fn();
render(
<SidebarProvider open={true} onOpenChange={onOpenChange}>
<Sidebar>x</Sidebar>
</SidebarProvider>,
);
await userEvent.keyboard("{Control>}b{/Control}");
expect(onOpenChange).toHaveBeenCalledWith(false);
});
});
describe("Sidebar", () => {
it("renders with data-slot='sidebar' when collapsible='none'", () => {
const { container } = render(
<SidebarProvider>
<Sidebar collapsible="none">x</Sidebar>
</SidebarProvider>,
);
expect(container.querySelector("[data-slot=sidebar]")).toBeInTheDocument();
});
it("renders with data-state='expanded' by default", () => {
const { container } = render(
<SidebarProvider defaultOpen>
<Sidebar>x</Sidebar>
</SidebarProvider>,
);
// data-state is on the desktop wrapper
const sidebar = container.querySelector("[data-slot=sidebar]");
expect(sidebar).toHaveAttribute("data-state", "expanded");
});
it("renders with data-state='collapsed' when defaultOpen=false", () => {
const { container } = render(
<SidebarProvider defaultOpen={false}>
<Sidebar>x</Sidebar>
</SidebarProvider>,
);
const sidebar = container.querySelector("[data-slot=sidebar]");
expect(sidebar).toHaveAttribute("data-state", "collapsed");
});
it.each(["sidebar", "floating", "inset"] as const)("supports variant=%s", (variant) => {
const { container } = render(
<SidebarProvider>
<Sidebar variant={variant}>x</Sidebar>
</SidebarProvider>,
);
const sidebar = container.querySelector("[data-slot=sidebar]");
expect(sidebar).toHaveAttribute("data-variant", variant);
});
it.each(["left", "right"] as const)("supports side=%s", (side) => {
const { container } = render(
<SidebarProvider>
<Sidebar side={side}>x</Sidebar>
</SidebarProvider>,
);
const sidebar = container.querySelector("[data-slot=sidebar]");
expect(sidebar).toHaveAttribute("data-side", side);
});
});
describe("SidebarTrigger", () => {
it("renders with sr-only label 'Toggle Sidebar'", () => {
render(
<SidebarProvider>
<SidebarTrigger />
</SidebarProvider>,
);
expect(screen.getByText("Toggle Sidebar")).toBeInTheDocument();
});
it("has data-slot='sidebar-trigger' and data-sidebar='trigger'", () => {
render(
<SidebarProvider>
<SidebarTrigger data-testid="trigger" />
</SidebarProvider>,
);
const trigger = screen.getByTestId("trigger");
expect(trigger).toHaveAttribute("data-slot", "sidebar-trigger");
expect(trigger).toHaveAttribute("data-sidebar", "trigger");
});
it("invokes user onClick before toggling", async () => {
const onClick = vi.fn();
const onOpenChange = vi.fn();
render(
<SidebarProvider open={true} onOpenChange={onOpenChange}>
<SidebarTrigger onClick={onClick} />
</SidebarProvider>,
);
await userEvent.click(screen.getByRole("button", { name: /toggle sidebar/i }));
expect(onClick).toHaveBeenCalled();
expect(onOpenChange).toHaveBeenCalledWith(false);
});
});
describe("SidebarRail", () => {
it("renders with data-slot='sidebar-rail' and aria-label='Toggle Sidebar'", () => {
render(
<SidebarProvider>
<SidebarRail />
</SidebarProvider>,
);
const rail = screen.getByLabelText("Toggle Sidebar");
expect(rail).toHaveAttribute("data-slot", "sidebar-rail");
});
it("toggles sidebar on click", async () => {
const onOpenChange = vi.fn();
render(
<SidebarProvider open={true} onOpenChange={onOpenChange}>
<SidebarRail />
</SidebarProvider>,
);
await userEvent.click(screen.getByLabelText("Toggle Sidebar"));
expect(onOpenChange).toHaveBeenCalledWith(false);
});
});
describe("SidebarInset", () => {
it("renders as <main> with data-slot='sidebar-inset'", () => {
render(<SidebarInset data-testid="inset">x</SidebarInset>);
const inset = screen.getByTestId("inset");
expect(inset.tagName).toBe("MAIN");
expect(inset).toHaveAttribute("data-slot", "sidebar-inset");
});
});
describe("SidebarInput", () => {
it("renders with data-slot='sidebar-input'", () => {
render(<SidebarInput data-testid="i" />);
expect(screen.getByTestId("i")).toHaveAttribute("data-slot", "sidebar-input");
});
});
describe("SidebarHeader / SidebarFooter / SidebarSeparator", () => {
it("SidebarHeader uses data-slot='sidebar-header'", () => {
render(<SidebarHeader data-testid="h">x</SidebarHeader>);
expect(screen.getByTestId("h")).toHaveAttribute("data-slot", "sidebar-header");
});
it("SidebarFooter uses data-slot='sidebar-footer'", () => {
render(<SidebarFooter data-testid="f">x</SidebarFooter>);
expect(screen.getByTestId("f")).toHaveAttribute("data-slot", "sidebar-footer");
});
it("SidebarSeparator uses data-slot='sidebar-separator'", () => {
render(<SidebarSeparator data-testid="s" />);
expect(screen.getByTestId("s")).toHaveAttribute("data-slot", "sidebar-separator");
});
});
describe("SidebarContent / SidebarGroup", () => {
it("SidebarContent uses data-slot='sidebar-content'", () => {
render(<SidebarContent data-testid="c">x</SidebarContent>);
expect(screen.getByTestId("c")).toHaveAttribute("data-slot", "sidebar-content");
});
it("SidebarGroup uses data-slot='sidebar-group'", () => {
render(<SidebarGroup data-testid="g">x</SidebarGroup>);
expect(screen.getByTestId("g")).toHaveAttribute("data-slot", "sidebar-group");
});
it("SidebarGroupContent uses data-slot='sidebar-group-content'", () => {
render(<SidebarGroupContent data-testid="g">x</SidebarGroupContent>);
expect(screen.getByTestId("g")).toHaveAttribute("data-slot", "sidebar-group-content");
});
it("SidebarGroupLabel uses data-slot='sidebar-group-label'", () => {
render(<SidebarGroupLabel data-testid="l">Group</SidebarGroupLabel>);
expect(screen.getByTestId("l")).toHaveAttribute("data-slot", "sidebar-group-label");
});
it("SidebarGroupAction uses data-slot='sidebar-group-action'", () => {
render(<SidebarGroupAction data-testid="a">+</SidebarGroupAction>);
expect(screen.getByTestId("a")).toHaveAttribute("data-slot", "sidebar-group-action");
});
});
describe("SidebarMenu and items", () => {
it("SidebarMenu uses data-slot='sidebar-menu' and renders as <ul>", () => {
render(<SidebarMenu data-testid="m" />);
const menu = screen.getByTestId("m");
expect(menu.tagName).toBe("UL");
expect(menu).toHaveAttribute("data-slot", "sidebar-menu");
});
it("SidebarMenuItem uses data-slot='sidebar-menu-item' and renders as <li>", () => {
render(<SidebarMenuItem data-testid="i">x</SidebarMenuItem>);
const item = screen.getByTestId("i");
expect(item.tagName).toBe("LI");
expect(item).toHaveAttribute("data-slot", "sidebar-menu-item");
});
it("SidebarMenuButton uses data-slot='sidebar-menu-button'", () => {
render(
<SidebarProvider>
<SidebarMenuButton data-testid="b">Click</SidebarMenuButton>
</SidebarProvider>,
);
expect(screen.getByTestId("b")).toHaveAttribute("data-slot", "sidebar-menu-button");
});
it("SidebarMenuButton supports isActive", () => {
render(
<SidebarProvider>
<SidebarMenuButton data-testid="b" isActive>
x
</SidebarMenuButton>
</SidebarProvider>,
);
// data-active is set as a presence attribute when isActive is truthy
expect(screen.getByTestId("b")).toHaveAttribute("data-active");
});
it("SidebarMenuAction uses data-slot='sidebar-menu-action'", () => {
render(<SidebarMenuAction data-testid="a">x</SidebarMenuAction>);
expect(screen.getByTestId("a")).toHaveAttribute("data-slot", "sidebar-menu-action");
});
it("SidebarMenuBadge uses data-slot='sidebar-menu-badge'", () => {
render(<SidebarMenuBadge data-testid="b">5</SidebarMenuBadge>);
expect(screen.getByTestId("b")).toHaveAttribute("data-slot", "sidebar-menu-badge");
});
it("SidebarMenuSkeleton renders without throwing", () => {
render(<SidebarMenuSkeleton data-testid="sk" />);
expect(screen.getByTestId("sk")).toBeInTheDocument();
});
it("SidebarMenuSkeleton supports showIcon", () => {
const { container } = render(<SidebarMenuSkeleton showIcon />);
// Should render the icon Skeleton
expect(container.querySelectorAll("[data-slot=skeleton]").length).toBeGreaterThan(1);
});
});
describe("SidebarMenuSub family", () => {
it("SidebarMenuSub uses data-slot='sidebar-menu-sub' and is <ul>", () => {
render(<SidebarMenuSub data-testid="ms" />);
const ms = screen.getByTestId("ms");
expect(ms.tagName).toBe("UL");
expect(ms).toHaveAttribute("data-slot", "sidebar-menu-sub");
});
it("SidebarMenuSubItem uses data-slot='sidebar-menu-sub-item' and is <li>", () => {
render(<SidebarMenuSubItem data-testid="ms">x</SidebarMenuSubItem>);
const ms = screen.getByTestId("ms");
expect(ms.tagName).toBe("LI");
expect(ms).toHaveAttribute("data-slot", "sidebar-menu-sub-item");
});
it("SidebarMenuSubButton uses data-slot='sidebar-menu-sub-button'", () => {
render(<SidebarMenuSubButton data-testid="b">x</SidebarMenuSubButton>);
expect(screen.getByTestId("b")).toHaveAttribute("data-slot", "sidebar-menu-sub-button");
});
});
describe("Sidebar composition", () => {
it("composes a complete sidebar without errors", () => {
expect(() =>
renderInProvider(
<>
<SidebarHeader>Header</SidebarHeader>
<SidebarSeparator />
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Group 1</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>Item</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>Footer</SidebarFooter>
</>,
),
).not.toThrow();
});
});
@@ -0,0 +1,27 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Skeleton } from "./skeleton";
describe("Skeleton", () => {
it("renders with data-slot='skeleton'", () => {
render(<Skeleton data-testid="sk" />);
expect(screen.getByTestId("sk")).toHaveAttribute("data-slot", "skeleton");
});
it("applies pulse animation classes", () => {
render(<Skeleton data-testid="sk" />);
expect(screen.getByTestId("sk")).toHaveClass("animate-pulse");
});
it("merges custom className", () => {
render(<Skeleton className="h-10 w-20" data-testid="sk" />);
const skeleton = screen.getByTestId("sk");
expect(skeleton).toHaveClass("h-10");
expect(skeleton).toHaveClass("w-20");
});
it("forwards arbitrary props", () => {
render(<Skeleton data-testid="sk" aria-busy="true" />);
expect(screen.getByTestId("sk")).toHaveAttribute("aria-busy", "true");
});
});
@@ -0,0 +1,43 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Slider } from "./slider";
describe("Slider", () => {
it("renders with data-slot='slider'", () => {
render(<Slider data-testid="s" defaultValue={[50]} />);
expect(screen.getByTestId("s")).toHaveAttribute("data-slot", "slider");
});
it("renders one thumb per value", () => {
const { container } = render(<Slider defaultValue={[10, 90]} />);
expect(container.querySelectorAll("[data-slot=slider-thumb]")).toHaveLength(2);
});
it("renders single thumb for single value", () => {
const { container } = render(<Slider defaultValue={[50]} />);
expect(container.querySelectorAll("[data-slot=slider-thumb]")).toHaveLength(1);
});
it("renders track and range", () => {
const { container } = render(<Slider defaultValue={[50]} />);
expect(container.querySelector("[data-slot=slider-track]")).toBeInTheDocument();
expect(container.querySelector("[data-slot=slider-range]")).toBeInTheDocument();
});
it("respects custom min and max", () => {
render(<Slider data-testid="s" defaultValue={[5]} min={0} max={10} />);
// Value, min, max are reflected on the root element via aria attrs in some envs
expect(screen.getByTestId("s")).toBeInTheDocument();
});
it("merges custom className", () => {
render(<Slider data-testid="s" defaultValue={[50]} className="custom-slider" />);
expect(screen.getByTestId("s")).toHaveClass("custom-slider");
});
it("renders 2 thumbs with default min/max if no defaultValue or value passed", () => {
const { container } = render(<Slider />);
// Source code: defaults to [min, max] when neither value nor defaultValue is array
expect(container.querySelectorAll("[data-slot=slider-thumb]")).toHaveLength(2);
});
});
@@ -0,0 +1,30 @@
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { ThemeProvider } from "next-themes";
import { Toaster } from "./sonner";
describe("Toaster", () => {
it("renders without errors when wrapped in ThemeProvider", () => {
const { container } = render(
<ThemeProvider>
<Toaster />
</ThemeProvider>,
);
// Sonner renders into document.body, but we just verify no crash
expect(container).toBeInTheDocument();
});
it("forwards arbitrary props to underlying Sonner", () => {
const { container } = render(
<ThemeProvider>
<Toaster position="top-right" />
</ThemeProvider>,
);
expect(container).toBeInTheDocument();
});
it("does not throw when rendered without theme context", () => {
// Toaster falls back to "system" theme when next-themes context is absent.
expect(() => render(<Toaster />)).not.toThrow();
});
});
@@ -0,0 +1,39 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Spinner } from "./spinner";
describe("Spinner", () => {
it("renders with role='status' for screen readers", () => {
render(<Spinner />);
expect(screen.getByRole("status")).toBeInTheDocument();
});
it("has accessible label 'Loading'", () => {
render(<Spinner />);
expect(screen.getByLabelText("Loading")).toBeInTheDocument();
});
it("merges custom className", () => {
render(<Spinner className="my-spinner" />);
expect(screen.getByRole("status")).toHaveClass("my-spinner");
});
it("applies animate-spin by default", () => {
render(<Spinner />);
expect(screen.getByRole("status")).toHaveClass("animate-spin");
});
it("uses currentColor by default (mapped to SVG fill/stroke by Phosphor)", () => {
render(<Spinner />);
const svg = screen.getByRole("status");
// Phosphor maps the `color` prop to inline style (fill/stroke), not an attribute.
// The hex/RGB or "currentColor" appears somewhere in style/attr — verify presence in serialized output.
expect(svg.outerHTML).toContain("currentColor");
});
it("respects color override", () => {
render(<Spinner color="red" />);
const svg = screen.getByRole("status");
expect(svg.outerHTML).toMatch(/red/);
});
});
@@ -0,0 +1,44 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Switch } from "./switch";
describe("Switch", () => {
it("renders with data-slot='switch'", () => {
render(<Switch data-testid="sw" />);
expect(screen.getByTestId("sw")).toHaveAttribute("data-slot", "switch");
});
it("defaults size to 'default'", () => {
render(<Switch data-testid="sw" />);
expect(screen.getByTestId("sw")).toHaveAttribute("data-size", "default");
});
it("supports size='sm'", () => {
render(<Switch data-testid="sw" size="sm" />);
expect(screen.getByTestId("sw")).toHaveAttribute("data-size", "sm");
});
it("starts unchecked by default", () => {
render(<Switch data-testid="sw" />);
expect(screen.getByTestId("sw").getAttribute("aria-checked")).toBe("false");
});
it("respects defaultChecked", () => {
render(<Switch data-testid="sw" defaultChecked />);
expect(screen.getByTestId("sw").getAttribute("aria-checked")).toBe("true");
});
it("calls onCheckedChange when toggled", async () => {
const onChange = vi.fn();
render(<Switch data-testid="sw" onCheckedChange={onChange} />);
await userEvent.click(screen.getByTestId("sw"));
expect(onChange).toHaveBeenCalledOnce();
expect(onChange).toHaveBeenCalledWith(true, expect.anything());
});
it("merges custom className", () => {
render(<Switch data-testid="sw" className="my-custom" />);
expect(screen.getByTestId("sw")).toHaveClass("my-custom");
});
});
+95
View File
@@ -0,0 +1,95 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
const renderTabs = (props: Partial<Parameters<typeof Tabs>[0]> = {}) => {
return render(
<Tabs defaultValue="a" {...props}>
<TabsList>
<TabsTrigger value="a">Tab A</TabsTrigger>
<TabsTrigger value="b">Tab B</TabsTrigger>
</TabsList>
<TabsContent value="a">Content A</TabsContent>
<TabsContent value="b">Content B</TabsContent>
</Tabs>,
);
};
describe("Tabs", () => {
it("uses data-slot='tabs'", () => {
const { container } = renderTabs();
expect(container.querySelector("[data-slot=tabs]")).toBeInTheDocument();
});
it("defaults orientation to 'horizontal'", () => {
const { container } = renderTabs();
expect(container.querySelector("[data-slot=tabs]")).toHaveAttribute("data-orientation", "horizontal");
});
it("supports vertical orientation", () => {
const { container } = renderTabs({ orientation: "vertical" });
expect(container.querySelector("[data-slot=tabs]")).toHaveAttribute("data-orientation", "vertical");
});
it("renders the default selected tab content", () => {
renderTabs();
expect(screen.getByText("Content A")).toBeInTheDocument();
});
it("switches to the other tab on click", async () => {
renderTabs();
await userEvent.click(screen.getByText("Tab B"));
expect(screen.getByText("Content B")).toBeInTheDocument();
});
it("merges custom className on root", () => {
const { container } = render(
<Tabs defaultValue="a" className="my-tabs">
<TabsList>
<TabsTrigger value="a">A</TabsTrigger>
</TabsList>
<TabsContent value="a">x</TabsContent>
</Tabs>,
);
expect(container.querySelector("[data-slot=tabs]")).toHaveClass("my-tabs");
});
});
describe("TabsList", () => {
it("uses data-slot='tabs-list'", () => {
const { container } = renderTabs();
expect(container.querySelector("[data-slot=tabs-list]")).toBeInTheDocument();
});
it("defaults variant to 'default'", () => {
const { container } = renderTabs();
expect(container.querySelector("[data-slot=tabs-list]")).toHaveAttribute("data-variant", "default");
});
it("supports variant='line'", () => {
const { container } = render(
<Tabs defaultValue="a">
<TabsList variant="line">
<TabsTrigger value="a">A</TabsTrigger>
</TabsList>
<TabsContent value="a">x</TabsContent>
</Tabs>,
);
expect(container.querySelector("[data-slot=tabs-list]")).toHaveAttribute("data-variant", "line");
});
});
describe("TabsTrigger", () => {
it("uses data-slot='tabs-trigger'", () => {
const { container } = renderTabs();
expect(container.querySelector("[data-slot=tabs-trigger]")).toBeInTheDocument();
});
});
describe("TabsContent", () => {
it("uses data-slot='tabs-content'", () => {
const { container } = renderTabs();
expect(container.querySelector("[data-slot=tabs-content]")).toBeInTheDocument();
});
});
@@ -0,0 +1,44 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Textarea } from "./textarea";
describe("Textarea", () => {
it("renders a <textarea> element", () => {
render(<Textarea data-testid="t" />);
expect(screen.getByTestId("t").tagName).toBe("TEXTAREA");
});
it("applies data-slot='textarea'", () => {
render(<Textarea data-testid="t" />);
expect(screen.getByTestId("t")).toHaveAttribute("data-slot", "textarea");
});
it("merges custom className", () => {
render(<Textarea data-testid="t" className="custom" />);
expect(screen.getByTestId("t")).toHaveClass("custom");
});
it("calls onChange when typing", async () => {
const onChange = vi.fn();
render(<Textarea data-testid="t" onChange={onChange} />);
await userEvent.type(screen.getByTestId("t"), "hi");
expect(onChange).toHaveBeenCalled();
});
it("respects disabled state", () => {
render(<Textarea data-testid="t" disabled />);
expect(screen.getByTestId("t")).toBeDisabled();
});
it("forwards rows and cols", () => {
render(<Textarea data-testid="t" rows={5} cols={40} />);
expect(screen.getByTestId("t")).toHaveAttribute("rows", "5");
expect(screen.getByTestId("t")).toHaveAttribute("cols", "40");
});
it("supports controlled value", () => {
render(<Textarea data-testid="t" value="hello" onChange={() => {}} />);
expect(screen.getByTestId("t")).toHaveValue("hello");
});
});
@@ -0,0 +1,63 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Toggle } from "./toggle";
describe("Toggle", () => {
it("renders with data-slot='toggle'", () => {
render(<Toggle data-testid="t">on</Toggle>);
expect(screen.getByTestId("t")).toHaveAttribute("data-slot", "toggle");
});
it("starts unpressed by default", () => {
render(<Toggle data-testid="t">on</Toggle>);
expect(screen.getByTestId("t").getAttribute("aria-pressed")).toBe("false");
});
it("can be controlled via pressed prop", () => {
render(
<Toggle data-testid="t" pressed>
on
</Toggle>,
);
expect(screen.getByTestId("t").getAttribute("aria-pressed")).toBe("true");
});
it("calls onPressedChange when clicked", async () => {
const onChange = vi.fn();
render(
<Toggle data-testid="t" onPressedChange={onChange}>
x
</Toggle>,
);
await userEvent.click(screen.getByTestId("t"));
expect(onChange).toHaveBeenCalledWith(true, expect.anything());
});
it.each([["default"], ["outline"]] as const)("renders variant=%s", (variant) => {
render(
<Toggle data-testid="t" variant={variant}>
x
</Toggle>,
);
expect(screen.getByTestId("t")).toBeInTheDocument();
});
it.each([["default"], ["sm"], ["lg"]] as const)("renders size=%s", (size) => {
render(
<Toggle data-testid="t" size={size}>
x
</Toggle>,
);
expect(screen.getByTestId("t")).toBeInTheDocument();
});
it("merges custom className", () => {
render(
<Toggle data-testid="t" className="my-custom">
x
</Toggle>,
);
expect(screen.getByTestId("t")).toHaveClass("my-custom");
});
});
@@ -0,0 +1,88 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
describe("Tooltip", () => {
it("trigger has data-slot='tooltip-trigger'", () => {
render(
<TooltipProvider>
<Tooltip>
<TooltipTrigger data-testid="trigger">Hover me</TooltipTrigger>
<TooltipContent>Helpful info</TooltipContent>
</Tooltip>
</TooltipProvider>,
);
expect(screen.getByTestId("trigger")).toHaveAttribute("data-slot", "tooltip-trigger");
});
it("does not show content by default", () => {
render(
<TooltipProvider>
<Tooltip>
<TooltipTrigger>Hover me</TooltipTrigger>
<TooltipContent>Helpful info</TooltipContent>
</Tooltip>
</TooltipProvider>,
);
expect(screen.queryByText("Helpful info")).not.toBeInTheDocument();
});
it("shows content when controlled open", () => {
render(
<TooltipProvider>
<Tooltip open>
<TooltipTrigger>Hover me</TooltipTrigger>
<TooltipContent>Helpful info</TooltipContent>
</Tooltip>
</TooltipProvider>,
);
expect(screen.getByText("Helpful info")).toBeInTheDocument();
});
it("content has data-slot='tooltip-content' when shown", () => {
render(
<TooltipProvider>
<Tooltip open>
<TooltipTrigger>Hover me</TooltipTrigger>
<TooltipContent>Helpful info</TooltipContent>
</Tooltip>
</TooltipProvider>,
);
expect(screen.getByText("Helpful info")).toHaveAttribute("data-slot", "tooltip-content");
});
});
describe("TooltipProvider", () => {
it("renders children without errors", () => {
render(
<TooltipProvider>
<div data-testid="child">child</div>
</TooltipProvider>,
);
expect(screen.getByTestId("child")).toBeInTheDocument();
});
it("accepts custom delay", () => {
render(
<TooltipProvider delay={500}>
<div>x</div>
</TooltipProvider>,
);
// No throw = pass
expect(true).toBe(true);
});
});
describe("TooltipContent", () => {
it("merges custom className when shown", () => {
render(
<TooltipProvider>
<Tooltip open>
<TooltipTrigger>x</TooltipTrigger>
<TooltipContent className="my-tooltip">visible</TooltipContent>
</Tooltip>
</TooltipProvider>,
);
expect(screen.getByText("visible")).toHaveClass("my-tooltip");
});
});