This skill should be used when writing, reviewing, or debugging TypeScript tests. It covers Vitest configuration, test organization, assertion patterns, mocking strategies, coverage analysis, parameterized tests, and test quality standards.
From mnpx claudepluginhub molcajeteai/plugin --plugin mThis skill uses the workspace's default tool permissions.
references/coverage.mdreferences/mocking.mdreferences/testing-patterns.mdreferences/vitest-config.mdSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Provides patterns for shared UI in Compose Multiplatform across Android, iOS, Desktop, and Web: state management with ViewModels/StateFlow, navigation, theming, and performance.
Quick reference for writing effective TypeScript tests with Vitest. Each section summarizes the key rules — reference files provide full examples and edge cases.
This project uses Vitest 4 with jsdom for React component tests and v8 for coverage.
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true, // describe, it, expect available without import
environment: "jsdom", // DOM environment for React tests
setupFiles: ["./setup.ts"], // Global setup (jest-dom matchers, mocks)
include: ["src/**/__tests__/**/*.test.{ts,tsx}"],
passWithNoTests: true, // Don't fail when no tests found
coverage: {
provider: "v8",
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
The shared test setup at components/web/src/test/setup.ts provides:
@testing-library/jest-dom/vitest — DOM assertion matchers (toBeInTheDocument, toHaveTextContent, etc.)ResizeObserver mock — Required for Radix UI componentsVitest resolve.alias must match tsconfig.json paths:
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@drzum/ui": path.resolve(__dirname, "../components/web/src"),
},
},
# Run all tests
pnpm run test
# Run tests for a specific app
pnpm --filter patient test
# Run a specific test file
pnpm --filter patient test -- src/components/__tests__/Button.test.tsx
# Watch mode
pnpm --filter patient test:watch
# With coverage
pnpm --filter patient test:coverage
See references/vitest-config.md for the full config, environment options, and CI settings.
Place test files in a __tests__/ sibling directory next to the file being tested:
src/components/AuthGuard/
├── AuthGuard.tsx
├── index.ts
└── __tests__/
└── AuthGuard.test.tsx
Do NOT place tests alongside source files:
# ❌ Wrong
src/components/AuthGuard/
├── AuthGuard.tsx
├── AuthGuard.test.tsx # ❌ Not in __tests__/ directory
└── index.ts
*.test.ts or *.test.tsx. Match the source file name.describe("ComponentName", ...) or describe("functionName", ...).it("creates ..."), it("renders ..."). No "should" — it's noise.describe / it Nestingdescribe("AuthGuard", () => {
describe("when user is authenticated", () => {
it("renders children", () => { /* ... */ });
it("does not redirect", () => { /* ... */ });
});
describe("when user is not authenticated", () => {
it("redirects to sign-in", () => { /* ... */ });
it("does not render children", () => { /* ... */ });
});
});
describe("UserService", () => {
let service: UserService;
beforeEach(() => {
service = new UserService(mockRepository);
});
afterEach(() => {
vi.restoreAllMocks();
});
it("creates a user", () => { /* ... */ });
});
beforeEach — Reset state for every test. Prefer this over beforeAll.afterEach — Clean up mocks and side effects.beforeAll / afterAll — Only for expensive setup that's safe to share (database connections, server start).Every test follows three phases:
it("formats a date in Mexican locale", () => {
// Arrange
const date = new Date("2024-03-15");
// Act
const result = formatDate(date);
// Assert
expect(result).toBe("15/03/2024");
});
it.each)Test multiple inputs with the same assertion logic:
it.each([
{ input: "", expected: false },
{ input: "invalid", expected: false },
{ input: "user@example.com", expected: true },
{ input: "user@example.co.mx", expected: true },
])("validates email '$input' as $expected", ({ input, expected }) => {
expect(isValidEmail(input)).toBe(expected);
});
// async/await
it("fetches user data", async () => {
const user = await fetchUser("123");
expect(user.name).toBe("Alice");
});
// Testing rejections
it("throws on invalid ID", async () => {
await expect(fetchUser("invalid")).rejects.toThrow("User not found");
});
import { renderWithI18n, screen } from "@drzum/ui/test";
import userEvent from "@testing-library/user-event";
describe("LoginForm", () => {
it("calls onSubmit with email and password", async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
renderWithI18n(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/correo/i), "test@example.com");
await user.type(screen.getByLabelText(/contraseña/i), "password123");
await user.click(screen.getByRole("button", { name: /iniciar sesión/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
});
});
});
See references/testing-patterns.md for factory functions, async patterns, and anti-patterns.
// Equality
expect(value).toBe(42); // strict equality (===)
expect(obj).toEqual({ id: "1", name: "A" }); // deep equality
expect(obj).toMatchObject({ id: "1" }); // partial match
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(5);
expect(value).toBeLessThanOrEqual(10);
expect(value).toBeCloseTo(0.3, 5);
// Strings
expect(value).toContain("substring");
expect(value).toMatch(/pattern/);
// Arrays
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
expect(arr).toEqual(expect.arrayContaining([1, 2]));
// Objects
expect(obj).toHaveProperty("name");
expect(obj).toHaveProperty("address.city", "CDMX");
// Resolves
await expect(asyncFn()).resolves.toBe(42);
// Rejects
await expect(asyncFn()).rejects.toThrow("error message");
await expect(asyncFn()).rejects.toBeInstanceOf(NotFoundError);
Available via @testing-library/jest-dom/vitest setup:
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeDisabled();
expect(element).toHaveTextContent("Hello");
expect(element).toHaveAttribute("aria-label", "Close");
expect(element).toHaveClass("active");
expect(input).toHaveValue("test@example.com");
// Exact message
expect(() => parse("invalid")).toThrow("Invalid input");
// Regex match
expect(() => parse("invalid")).toThrow(/invalid/i);
// Error type
expect(() => parse("invalid")).toThrow(ValidationError);
vi.fn() — Mock Functionsconst mockFn = vi.fn();
mockFn("hello");
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith("hello");
expect(mockFn).toHaveBeenCalledTimes(1);
// Return values
const mockGet = vi.fn().mockReturnValue(42);
const mockFetch = vi.fn().mockResolvedValue({ data: [] });
const mockSave = vi.fn().mockRejectedValue(new Error("fail"));
// Implementation
const mockFn = vi.fn((x: number) => x * 2);
vi.spyOn() — Spy on Methodsconst spy = vi.spyOn(console, "error").mockImplementation(() => {});
doSomething();
expect(spy).toHaveBeenCalledWith("expected error message");
spy.mockRestore(); // restore original
vi.mock() — Module Mocking// Auto-mock all exports
vi.mock("./api");
// Manual mock factory
vi.mock("./auth", () => ({
useAuth: vi.fn(() => ({
user: { id: "1", name: "Test" },
isAuthenticated: true,
})),
}));
// Partial mock — keep real implementations, override specific exports
vi.mock("./utils", async (importOriginal) => {
const actual = await importOriginal<typeof import("./utils")>();
return {
...actual,
formatDate: vi.fn(() => "01/01/2024"),
};
});
vi.clearAllMocks() — Clears call history and return values. Mock still exists.vi.resetAllMocks() — Clears + removes return values and implementations.vi.restoreAllMocks() — Resets + restores original implementations for spies.Best practice: Use vi.restoreAllMocks() in afterEach to prevent test pollution:
afterEach(() => {
vi.restoreAllMocks();
});
See references/mocking.md for timer mocks, external module mocking, mock assertions, and anti-patterns.
# Coverage for all apps
pnpm run test:coverage
# Coverage for specific app
pnpm --filter patient test:coverage
This project enforces 80% minimums:
| Metric | Threshold |
|---|---|
| Lines | 80% |
| Functions | 80% |
| Branches | 80% |
| Statements | 80% |
.d.ts files)index.ts re-exports)vite.config.ts, vitest.config.ts)See references/coverage.md for coverage config, CI integration, and what to exclude.
// ❌ Wrong — testing implementation details
it("calls setState with the new value", () => {
const setState = vi.spyOn(React, "useState");
// ...
expect(setState).toHaveBeenCalledWith("new value");
});
// ✅ Correct — testing observable behavior
it("displays the updated value", async () => {
const user = userEvent.setup();
renderWithI18n(<Counter />);
await user.click(screen.getByRole("button", { name: /incrementar/i }));
expect(screen.getByText("1")).toBeInTheDocument();
});
Each test should verify one logical concept. Multiple expect calls are fine when they assert on the same outcome:
// ✅ Good — multiple expects for one concept
it("creates a user with correct defaults", () => {
const user = createUser({ name: "Alice" });
expect(user.name).toBe("Alice");
expect(user.role).toBe("patient");
expect(user.isActive).toBe(true);
});
// ❌ Bad — unrelated assertions mixed together
it("works correctly", () => {
expect(createUser({ name: "A" }).name).toBe("A");
expect(deleteUser("123")).toBe(true);
expect(listUsers()).toHaveLength(0);
});
if, for, or switch in test code. Tests should be linear.After writing or modifying tests, always run the full verification protocol from the typescript-writing-code skill:
pnpm --filter <app> validate
# or: pnpm run type-check && pnpm run lint && pnpm run format && pnpm run test
All 4 steps must pass. See typescript-writing-code skill for details.
| File | Description |
|---|---|
| references/vitest-config.md | Full vitest config, environments, setup files, path aliases, CI settings |
| references/testing-patterns.md | AAA pattern, parameterized tests, async testing, factory functions, anti-patterns |
| references/mocking.md | vi.fn, vi.spyOn, vi.mock, timer mocks, module mocking, reset/restore |
| references/coverage.md | Coverage commands, thresholds, exclusions, CI integration |