Testing Guide
Comprehensive guide to testing applications built with @hanzo/ui.
Prerequisites
- Node.js 18+
- pnpm (recommended) or npm
- Playwright for E2E tests (optional)
Unit Testing
Setup with Vitest
npm install -D vitest @testing-library/react @testing-library/user-event happy-dom
import react from "@vitejs/plugin-react"
import { defineConfig } from "vitest/config"
export default defineConfig({
plugins: [react()],
test: {
environment: "happy-dom",
setupFiles: ["./vitest.setup.ts"],
},
})
import "@testing-library/jest-dom"
Testing Components
import { Button } from "@hanzo/ui"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
describe("Button", () => {
it("renders correctly", () => {
render(<Button>Click me</Button>)
expect(screen.getByRole("button")).toHaveTextContent("Click me")
})
it("handles click events", async () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Click me</Button>)
await userEvent.click(screen.getByRole("button"))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it("applies variants correctly", () => {
render(<Button variant="destructive">Delete</Button>)
expect(screen.getByRole("button")).toHaveClass("bg-destructive")
})
})
Testing Forms
import { Button, Form, Input } from "@hanzo/ui"
import { render, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { useForm } from "react-hook-form"
function LoginForm({ onSubmit }) {
const form = useForm()
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Input {...form.register("email")} type="email" />
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
describe("LoginForm", () => {
it("submits form data", async () => {
const handleSubmit = vi.fn()
render(<LoginForm onSubmit={handleSubmit} />)
await userEvent.type(screen.getByRole("textbox"), "test@example.com")
await userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith(
{
email: "test@example.com",
},
expect.anything()
)
})
})
})
End-to-End Testing
Setup with Playwright
npm install -D @playwright/test
npx playwright install
import { defineConfig, devices } from "@playwright/test"
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
use: {
baseURL: "http://localhost:3003",
trace: "on-first-retry",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
],
webServer: {
command: "npm run dev",
url: "http://localhost:3003",
reuseExistingServer: !process.env.CI,
},
})
E2E Test Examples
import { expect, test } from "@playwright/test"
test.describe("Navigation", () => {
test("homepage loads correctly", async ({ page }) => {
await page.goto("/")
await expect(page).toHaveTitle(/Hanzo UI/)
await expect(page.locator("h1")).toBeVisible()
})
test("component page navigates", async ({ page }) => {
await page.goto("/docs/components/button")
await expect(page.locator("h1")).toContainText("Button")
// Test component preview
const preview = page.locator("[data-rehype-pretty-code-fragment]")
await expect(preview).toBeVisible()
})
})
Testing Interactive Components
import { expect, test } from "@playwright/test"
test("dialog opens and closes", async ({ page }) => {
await page.goto("/docs/components/dialog")
// Click trigger button
await page.click("text=Open Dialog")
// Check dialog is visible
await expect(page.locator('[role="dialog"]')).toBeVisible()
// Close dialog
await page.click('[aria-label="Close"]')
// Check dialog is hidden
await expect(page.locator('[role="dialog"]')).not.toBeVisible()
})
Visual Regression Testing
import { expect, test } from "@playwright/test"
test.describe("Visual Regression", () => {
test("button variants", async ({ page }) => {
await page.goto("/docs/components/button")
const button = page.locator('[data-variant="default"]')
await expect(button).toHaveScreenshot("button-default.png")
})
test("dark mode theme", async ({ page }) => {
await page.goto("/")
await page.click("[data-theme-toggle]")
await expect(page).toHaveScreenshot("dark-mode.png", {
fullPage: true,
})
})
})
Accessibility Testing
import { Button } from "@hanzo/ui"
import { render } from "@testing-library/react"
import { axe, toHaveNoViolations } from "jest-axe"
expect.extend(toHaveNoViolations)
test("Button has no accessibility violations", async () => {
const { container } = render(<Button>Click me</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
Running Tests
Development
# Run all tests
pnpm test
# Run in watch mode
pnpm test:watch
# Run with coverage
pnpm test:coverage
# Run specific file
pnpm test button.test.tsx
End-to-End
# Run E2E tests
pnpm test:e2e
# Run in headed mode
pnpm test:e2e --headed
# Run in UI mode
pnpm test:e2e --ui
# Generate report
pnpm test:e2e
npx playwright show-report
CI/CD
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- run: pnpm install
- run: pnpm test
- run: pnpm test:e2e
Best Practices
-
Test User Behavior, Not Implementation
// Good await userEvent.click(screen.getByRole("button")) // Bad fireEvent.click(getByTestId("submit-btn"))
-
Use Semantic Queries
screen.getByRole("button", { name: /submit/i }) screen.getByLabelText("Email")
-
Test Accessibility
- Always include axe tests
- Test keyboard navigation
- Verify ARIA attributes
-
Mock External Dependencies
vi.mock("@/api", () => ({ fetchData: vi.fn(() => Promise.resolve(mockData)), }))
-
Use Data Attributes for Test IDs
<Button data-testid="submit-btn">Submit</Button>
Troubleshooting
Tests Fail Intermittently
- Use
waitFor
for async operations - Increase timeout for slow operations
- Check for race conditions
Snapshot Tests Break Often
- Use visual regression instead
- Keep snapshots small and focused
- Update snapshots carefully
Playwright Can't Find Elements
- Wait for navigation:
await page.waitForURL()
- Wait for selectors:
await page.waitForSelector()
- Use more specific selectors