tac-tdd
MANDATORY for all non-trivial implementation in TAC Express. Enforces RED-GREEN-REFACTOR cycle with Vitest and Playwright. Write the failing test first, always.
SKILL.md.claude/skills/tac-tdd/SKILL.md
GitHubTAC Express — Test-Driven Development
Invoke this skill before writing any implementation code. The cycle is always: RED → GREEN → REFACTOR → COMMIT
Iron Law: Production code without a prior failing test is not TDD. Delete it and start over.
The Cycle
1. Write failing test ← RED — test must fail for the right reason
2. Run test, confirm fail
3. Write minimal code ← GREEN — only enough to pass
4. Run test, confirm pass
5. Refactor ← clean up without breaking
6. Run test, still green
7. Commit
Repeat per feature/behavior. Never batch multiple RED→GREEN cycles without commits.
Test File Conventions
Location: Alongside source file
Naming: ComponentName.test.tsx | featureName.service.test.ts
Framework: Vitest (unit) | Playwright (E2E)
Unit Test Template (Component)
import { describe, it, expect, vi } from "vitest"
import { render, screen } from "@testing-library/react"
import { userEvent } from "@testing-library/user-event"
import { ComponentName } from "./ComponentName"
describe("ComponentName", () => {
it("renders with default variant", () => {
render(<ComponentName>content</ComponentName>)
expect(screen.getByText("content")).toBeInTheDocument()
})
it("applies glass variant correctly", () => {
const { container } = render(<ComponentName variant="glass" />)
expect(container.firstChild).toHaveAttribute("data-slot", "component-name")
})
it("calls onClick when clicked", async () => {
const user = userEvent.setup()
const onClick = vi.fn()
render(<ComponentName onClick={onClick}>click me</ComponentName>)
await user.click(screen.getByText("click me"))
expect(onClick).toHaveBeenCalledOnce()
})
})
Unit Test Template (Service)
import { describe, it, expect, vi, beforeEach } from "vitest"
import { createClient } from "@workspace/database"
import { getShipments } from "./shipment.service"
vi.mock("@workspace/database")
describe("shipment.service", () => {
const mockDb = {
from: vi.fn().mockReturnThis(),
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockResolvedValue({ data: [], error: null }),
}
beforeEach(() => {
vi.mocked(createClient).mockReturnValue(mockDb as any)
})
it("returns empty array when no shipments", async () => {
const result = await getShipments({ userId: "user-1" })
expect(result).toEqual([])
})
it("throws on database error", async () => {
mockDb.eq.mockResolvedValue({ data: null, error: { message: "DB error" } })
await expect(getShipments({ userId: "user-1" })).rejects.toThrow("DB error")
})
})
What to Test
Always Test (Components)
- Default render (snapshot optional)
- Each CVA variant produces correct
data-slot - Interactive states (click, focus, keyboard)
- Error/loading/empty states
- Accessibility:
aria-label,role, keyboard navigation
Always Test (Services)
- Happy path with mocked DB
- Error from database → throws with message
- Invalid input → throws or returns typed error
- Data transformation (input → output shape)
Always Test (Hooks)
- Initial state
- State after async operation (use
act) - Cleanup on unmount
Never Test
- CSS class names directly (fragile, not behavior)
- Implementation details (private functions)
- Third-party library behavior (they have their own tests)
- Snapshot tests without intent (use sparingly)
Mocking Rules
Mock at the boundary only:
✅ Mock packages/database in service tests
✅ Mock packages/services in component tests
❌ Never mock internal implementation details
❌ Never add test-only methods to production code
Mock Location
packages/services/__mocks__/ ← auto-mocked services
packages/database/__mocks__/ ← auto-mocked db client
Running Tests
# From monorepo root only:
pnpm test # all packages
pnpm test --filter ui # specific package
pnpm test -- --watch # watch mode
# E2E (Playwright):
pnpm test:e2e
Quality Gate Before Commit
pnpm test # all tests pass
pnpm typecheck # zero TypeScript errors
pnpm lint --max-warnings 0 # zero lint warnings
All three must be green. No exceptions.
Anti-Patterns to Reject
| Anti-pattern | Why | Fix |
|---|---|---|
it.skip(...) without comment |
Masks failures | Add // TODO: TAC-XXX — reason |
| Testing mock behavior | Not real behavior | Test real outputs |
expect(true).toBe(true) |
Always passes | Write meaningful assertion |
| Test written after implementation | Not TDD | Delete impl, write test first |
| Giant test file > 300 lines | Too broad scope | Split into focused test files |
TAC-Specific Testing Notes
- Violet Grid components: test
data-slotattributes, not class names - Services: always mock
@workspace/database, never real Supabase in unit tests - Icons: mock
@workspace/ui/iconsif testing components that use icons - Tokens: don't test CSS variable values — they're design-time, not runtime