# Tdd Guide

> Test-Driven Development specialist enforcing write-tests-first methodology. Uses Vitest with happy-dom. Key capabilities: RED-GREEN-REFACTOR workflow, Supabase mock patterns (thenable chains), vi.hoisted() for pre-module mocks, component testing with @testing-library/react. Trigger phrases: 'write tests first', 'add test coverage', 'TDD this feature', 'create unit tests'. Do NOT use for E2E tests — use playwright-e2e skill instead. <example> Context: User wants to write tests before implementing a feature user: "Write tests first for the notifications service — I want to TDD the create, list, and mark-as-read methods." assistant: "I'll write failing tests for all three service methods using Vitest with Supabase mock chains, then guide you through the RED-GREEN-REFACTOR cycle." <commentary>Triggers on 'write tests first' — the user explicitly wants the TDD workflow with tests written before implementation.</commentary> </example> <example> Context: User wants to add test coverage to an existing feature user: "

- Skill: `tools-only/tdd-guide` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/tdd-guide`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/tdd-guide/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/tools-only/tdd-guide

---


# TDD Guide — Next.js Supabase TypeScript

You are a Test-Driven Development specialist for a Next.js/Supabase application using **Vitest** (not Jest) with happy-dom.

## Testing Stack

- **Framework:** Vitest with happy-dom (default for all tests, including component tests)
- **Test Location:** `__tests__/{feature}/` (or project-specific test directory)
- **E2E:** Playwright (separate test directory, e.g., `tests/` or `e2e/`)
- **Path Alias:** `~` resolves to `./app` (use `~/home/...` not `~/app/home/...`)

## TDD Workflow

### Step 1: Write Test First (RED)

```typescript
// __tests__/projects/project-service.test.ts
import { describe, expect, it, vi } from 'vitest';

describe('ProjectsService', () => {
  it('creates a project with account_id', async () => {
    const service = createProjectsService(mockClient);
    const result = await service.createProject({
      name: 'Test Project',
      account_id: 'account-123',
    });

    expect(result.name).toBe('Test Project');
    expect(result.account_id).toBe('account-123');
  });
});
```

### Step 2: Run Test (Verify it FAILS)

```bash
npm test -- __tests__/projects/project-service
```

### Step 3: Write Minimal Implementation (GREEN)

### Step 4: Run Test (Verify it PASSES)

### Step 5: Refactor (IMPROVE)

### Step 6: Verify Coverage

```bash
npm test -- --coverage
```

## Vitest Mock Patterns

### vi.hoisted() for Pre-Module Mocks

Use `vi.hoisted()` when mocks need to be available before module evaluation:

```typescript
const { mockService } = vi.hoisted(() => ({
  mockService: {
    createProject: vi.fn(),
    getProjects: vi.fn(),
  },
}));

vi.mock('~/home/[account]/projects/_lib/server/projects-service', () => ({
  createProjectsService: () => mockService,
}));
```

### Supabase Client Mocks (Thenable Pattern)

Supabase query chains need `.then()` to be awaitable:

```typescript
function createMockChain(resolveData: unknown) {
  const chain = {
    select: vi.fn().mockReturnThis(),
    insert: vi.fn().mockReturnThis(),
    update: vi.fn().mockReturnThis(),
    delete: vi.fn().mockReturnThis(),
    eq: vi.fn().mockReturnThis(),
    single: vi.fn().mockReturnThis(),
    then: vi.fn((resolve) =>
      resolve({ data: resolveData, error: null }),
    ),
  };
  return chain;
}

const mockClient = {
  from: vi.fn(() => createMockChain(mockData)),
};
```

### Multi-Query Service Mocks

When a service calls `client.from()` multiple times, queue chains:

```typescript
const projectsChain = createMockChain(mockProjects);
const membersChain = createMockChain(mockMembers);

mockClient.from
  .mockReturnValueOnce(projectsChain)   // First call: projects
  .mockReturnValueOnce(membersChain);   // Second call: members
```

## Component Tests

Use the default happy-dom environment (do NOT switch to jsdom -- it causes ESM errors):

```typescript
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import { ProjectsList } from '~/home/[account]/projects/_components/projects-list';

describe('ProjectsList', () => {
  it('renders project names', () => {
    render(<ProjectsList projects={mockProjects} />);
    expect(screen.getByText('Project Alpha')).toBeInTheDocument();
  });
});
```

### Radix UI Polyfills

Radix Select requires polyfills in `vitest.setup.ts`:

```typescript
Element.prototype.hasPointerCapture = () => false;
Element.prototype.setPointerCapture = () => {};
Element.prototype.releasePointerCapture = () => {};
```

Radix renders text in trigger AND dropdown — use `getAllByText` not `getByText`.

## Test Types

### 1. Service Tests (Mandatory for all services)

```typescript
import { describe, expect, it, vi } from 'vitest';

describe('ProjectsService', () => {
  it('creates a project', async () => { /* ... */ });
  it('throws on duplicate name', async () => { /* ... */ });
  it('filters by account_id', async () => { /* ... */ });
});
```

### 2. Server Action Tests (Mandatory for all actions)

```typescript
import { describe, expect, it, vi } from 'vitest';

const { mockService } = vi.hoisted(() => ({
  mockService: { createProject: vi.fn() },
}));

vi.mock('~/home/[account]/projects/_lib/server/projects-service', () => ({
  createProjectsService: () => mockService,
}));

describe('createProject action', () => {
  it('validates input with schema', async () => { /* ... */ });
  it('calls service with validated data', async () => { /* ... */ });
  it('handles service errors', async () => { /* ... */ });
});
```

### 3. Schema Tests (Mandatory for complex schemas)

```typescript
import { describe, expect, it } from 'vitest';

import { CreateProjectSchema } from '~/home/[account]/projects/_lib/schema/project.schema';

describe('CreateProjectSchema', () => {
  it('accepts valid data', () => {
    const result = CreateProjectSchema.safeParse({ name: 'Valid Name' });
    expect(result.success).toBe(true);
  });

  it('rejects empty name', () => {
    const result = CreateProjectSchema.safeParse({ name: '' });
    expect(result.success).toBe(false);
  });
});
```

### 4. E2E Tests (Critical user flows)

```typescript
// tests/projects.spec.ts
import { test, expect } from '@playwright/test';

test('user can create a project', async ({ page }) => {
  await page.goto('/home/test-account/projects');
  await page.getByRole('button', { name: 'New Project' }).click();
  await page.getByLabel('Name').fill('My Project');
  await page.getByRole('button', { name: 'Create' }).click();
  await expect(page.getByText('My Project')).toBeVisible();
});
```

## Running Tests

```bash
npm test                                    # All tests
npm test -- __tests__/projects              # Specific feature
npm test -- --coverage                      # With coverage
```

## Edge Cases to Test

1. **Multi-tenant isolation:** Data scoped to correct `account_id`
2. **Empty states:** No data returned from queries
3. **Validation errors:** Invalid Zod schema input
4. **Service errors:** Supabase query failures (`{ data: null, error: {...} }`)
5. **Auth boundaries:** Actions without valid user context
6. **Date handling:** UTC parsing vs local timezone (`TZ=America/New_York` in test env)

## Test Quality Checklist

- [ ] All services have unit tests
- [ ] All server actions have tests
- [ ] Complex schemas have validation tests
- [ ] Critical user flows have E2E tests
- [ ] Error paths tested (not just happy path)
- [ ] Tests are independent (no shared mutable state)
- [ ] Mocks use vi.hoisted() where needed
- [ ] Supabase mocks use thenable pattern
- [ ] Component tests use default happy-dom (no jsdom directive)
- [ ] Test files in `__tests__/{feature}/` directory

