- Vitest multi-project setup (unit / integration / component) - Real-Postgres integration harness via in-process PGlite (TEST_DATABASE_URL override), migrations applied per worker; production code untouched - Unit: routing, locale, validation/Zod schemas, metadata, mail, site-theme, marquee, media, portfolio helpers, plus architecture-rule tests - Integration: Prisma data layer, API routes, and all server actions - Component: UI primitives and form components (jsdom + Testing Library) - 371 tests passing
150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
import { createRef } from "react";
|
|
|
|
import { render, screen } from "@testing-library/react";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { AppCard } from "@/components/ui/app-card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Container } from "@/components/layout/container";
|
|
import { HeroBadge } from "@/components/layout/hero-badge";
|
|
import { SectionHeading } from "@/components/home/section-heading";
|
|
|
|
describe("Badge", () => {
|
|
it("renders children and default variant classes", () => {
|
|
render(<Badge>New</Badge>);
|
|
const badge = screen.getByText("New");
|
|
expect(badge).toHaveClass("bg-primary");
|
|
});
|
|
|
|
it("applies a chosen variant and merges custom classNames", () => {
|
|
render(<Badge variant="success" className="custom-class">OK</Badge>);
|
|
const badge = screen.getByText("OK");
|
|
expect(badge).toHaveClass("bg-status-success-soft");
|
|
expect(badge).toHaveClass("custom-class");
|
|
});
|
|
|
|
it("passes through arbitrary props", () => {
|
|
render(<Badge data-testid="b" title="tip">X</Badge>);
|
|
expect(screen.getByTestId("b")).toHaveAttribute("title", "tip");
|
|
});
|
|
});
|
|
|
|
describe("Input", () => {
|
|
it("forwards the ref and renders attributes", () => {
|
|
const ref = createRef<HTMLInputElement>();
|
|
render(<Input ref={ref} type="email" placeholder="you@example.com" disabled />);
|
|
const input = screen.getByPlaceholderText("you@example.com");
|
|
expect(ref.current).toBe(input);
|
|
expect(input).toHaveAttribute("type", "email");
|
|
expect(input).toBeDisabled();
|
|
});
|
|
|
|
it("merges custom classes", () => {
|
|
render(<Input className="w-20" aria-label="field" />);
|
|
expect(screen.getByLabelText("field")).toHaveClass("w-20");
|
|
});
|
|
});
|
|
|
|
describe("Textarea", () => {
|
|
it("forwards the ref and renders a default value", () => {
|
|
const ref = createRef<HTMLTextAreaElement>();
|
|
render(<Textarea ref={ref} defaultValue="hello" aria-label="msg" />);
|
|
const textarea = screen.getByLabelText("msg");
|
|
expect(ref.current).toBe(textarea);
|
|
expect(textarea).toHaveValue("hello");
|
|
});
|
|
});
|
|
|
|
describe("Label", () => {
|
|
it("associates with a control via htmlFor", () => {
|
|
render(<Label htmlFor="name">Name</Label>);
|
|
expect(screen.getByText("Name")).toHaveAttribute("for", "name");
|
|
});
|
|
});
|
|
|
|
describe("Separator", () => {
|
|
it("defaults to a horizontal separator", () => {
|
|
const { container } = render(<Separator />);
|
|
expect(container.firstChild).toHaveClass("h-px", "w-full");
|
|
});
|
|
|
|
it("supports a vertical orientation", () => {
|
|
const { container } = render(<Separator orientation="vertical" />);
|
|
expect(container.firstChild).toHaveClass("h-full", "w-px");
|
|
});
|
|
});
|
|
|
|
describe("Card family", () => {
|
|
it("composes header, title, description and content", () => {
|
|
render(
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Title</CardTitle>
|
|
<CardDescription>Desc</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>Body</CardContent>
|
|
</Card>,
|
|
);
|
|
expect(screen.getByRole("heading", { name: "Title" })).toBeInTheDocument();
|
|
expect(screen.getByText("Desc")).toBeInTheDocument();
|
|
expect(screen.getByText("Body")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("AppCard", () => {
|
|
it("renders a single-layer card with its children", () => {
|
|
render(<AppCard layer="single">Single</AppCard>);
|
|
expect(screen.getByText("Single")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders a double-layer card (default) with its children", () => {
|
|
render(<AppCard>Double</AppCard>);
|
|
expect(screen.getByText("Double")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Container", () => {
|
|
it("applies the size variant classes", () => {
|
|
const { container } = render(<Container size="narrow">Body</Container>);
|
|
expect(container.firstChild).toHaveClass("max-w-narrow");
|
|
});
|
|
|
|
it("renders as a child element when asChild is set", () => {
|
|
render(
|
|
<Container asChild>
|
|
<section data-testid="as-child">X</section>
|
|
</Container>,
|
|
);
|
|
expect(screen.getByTestId("as-child").tagName).toBe("SECTION");
|
|
});
|
|
});
|
|
|
|
describe("HeroBadge", () => {
|
|
it("renders children and optional trailing content", () => {
|
|
render(<HeroBadge trailing={<span>→</span>}>Available</HeroBadge>);
|
|
expect(screen.getByText("Available")).toBeInTheDocument();
|
|
expect(screen.getByText("→")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("SectionHeading", () => {
|
|
it("renders eyebrow, title and description", () => {
|
|
render(<SectionHeading eyebrow="Work" title="Projects" description="What I build" />);
|
|
expect(screen.getByText("Work")).toBeInTheDocument();
|
|
expect(screen.getByRole("heading", { name: "Projects" })).toBeInTheDocument();
|
|
expect(screen.getByText("What I build")).toBeInTheDocument();
|
|
});
|
|
|
|
it("centers content when align is center", () => {
|
|
const { container } = render(
|
|
<SectionHeading eyebrow="E" title="T" description="D" align="center" />,
|
|
);
|
|
expect(container.firstChild).toHaveClass("text-center");
|
|
});
|
|
});
|