AgentSkillsCN

Test

测试

SKILL.md

Test Skill

Run tests and ensure code quality meets the 80% coverage requirement.

Trigger

Use this skill when: user says "test", "run tests", "check coverage", "write tests", or "verify quality"

Quick Commands

bash
# Run all tests
pnpm test

# Run tests with coverage report
pnpm test:coverage

# Run tests in watch mode
pnpm test -- --watch

# Run specific test file
pnpm test path/to/test.test.tsx

# Run tests matching pattern
pnpm test -- -t "pattern"

# Open Vitest UI
pnpm test:ui

Coverage Requirements

Minimum 80% coverage required for:

  • Statements
  • Branches
  • Functions
  • Lines

The CI/CD pipeline will fail if coverage drops below 80%.

Writing Tests

Test File Location

Tests go in __tests__/ directory, mirroring source structure:

code
__tests__/
├── components/
│   ├── button.test.tsx
│   └── header.test.tsx
├── hooks/
│   └── use-auth.test.tsx
├── lib/
│   └── utils.test.ts
└── app/
    └── page.test.tsx

Component Test Template

tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { ComponentName } from "@/components/path/component-name";

// Mock dependencies if needed
vi.mock("next/navigation", () => ({
  useRouter: () => ({ push: vi.fn() }),
}));

describe("ComponentName", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("renders correctly", () => {
    render(<ComponentName />);
    expect(screen.getByRole("button")).toBeInTheDocument();
  });

  it("handles user interaction", async () => {
    const user = userEvent.setup();
    const onClick = vi.fn();

    render(<ComponentName onClick={onClick} />);
    await user.click(screen.getByRole("button"));

    expect(onClick).toHaveBeenCalledTimes(1);
  });

  it("displays loading state", () => {
    render(<ComponentName isLoading />);
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it("shows error message on failure", () => {
    render(<ComponentName error="Something went wrong" />);
    expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
  });
});

Hook Test Template

tsx
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { useCustomHook } from "@/hooks/use-custom-hook";

describe("useCustomHook", () => {
  it("returns initial state", () => {
    const { result } = renderHook(() => useCustomHook());
    expect(result.current.value).toBe(initialValue);
  });

  it("updates state correctly", () => {
    const { result } = renderHook(() => useCustomHook());

    act(() => {
      result.current.setValue("new value");
    });

    expect(result.current.value).toBe("new value");
  });
});

Async Test Template

tsx
import { render, screen, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";

// Mock fetch or API calls
vi.mock("@/lib/api", () => ({
  fetchData: vi.fn(),
}));

import { fetchData } from "@/lib/api";
import { DataComponent } from "@/components/data-component";

describe("DataComponent", () => {
  it("fetches and displays data", async () => {
    vi.mocked(fetchData).mockResolvedValue({ items: ["Item 1", "Item 2"] });

    render(<DataComponent />);

    // Wait for data to load
    await waitFor(() => {
      expect(screen.getByText("Item 1")).toBeInTheDocument();
    });
  });

  it("handles errors gracefully", async () => {
    vi.mocked(fetchData).mockRejectedValue(new Error("Failed"));

    render(<DataComponent />);

    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument();
    });
  });
});

What to Test

Always Test

  • Component rendering
  • User interactions (clicks, typing, etc.)
  • Loading states
  • Error states
  • Edge cases (empty data, null values)
  • Accessibility (roles, labels)

Don't Test

  • Third-party library internals
  • CSS styling (unless critical)
  • Implementation details
  • Trivial code (getters, simple wrappers)

Mocking

Mock Next.js Navigation

tsx
vi.mock("next/navigation", () => ({
  useRouter: () => ({
    push: vi.fn(),
    replace: vi.fn(),
    back: vi.fn(),
  }),
  usePathname: () => "/",
  useSearchParams: () => new URLSearchParams(),
}));

Mock Supabase

tsx
vi.mock("@/supabase/client", () => ({
  createClient: () => ({
    from: vi.fn(() => ({
      select: vi.fn().mockResolvedValue({ data: [], error: null }),
      insert: vi.fn().mockResolvedValue({ data: null, error: null }),
    })),
    auth: {
      getUser: vi.fn().mockResolvedValue({ data: { user: null }, error: null }),
    },
  }),
}));

Mock Next-Themes

tsx
vi.mock("next-themes", () => ({
  useTheme: () => ({ theme: "light", setTheme: vi.fn() }),
  ThemeProvider: ({ children }: { children: React.ReactNode }) => children,
}));

Debugging Tests

bash
# Run with verbose output
pnpm test -- --reporter=verbose

# Run single test in isolation
pnpm test -- path/to/test.test.tsx --run

# Debug in browser
pnpm test:ui

Checklist Before PR

  • All tests pass: pnpm test
  • Coverage >= 80%: pnpm test:coverage
  • No console errors/warnings
  • Lint passes: pnpm lint
  • Build succeeds: pnpm build