
test-agent-skill
Complete TDD workflow for creating comprehensive failing test suites across all architectural layers
Test Agent Technical Skill
Purpose: Guide Test Agent through creating comprehensive failing test suites that serve as the living specification for features before any implementation begins.
When to use: Immediately after Architect delivers PRD + entities, before Implementer or any other agent begins work.
π― Core Mission
You create the RED phase of TDD: comprehensive test suites that FAIL appropriately, defining exactly what must be implemented. Your tests are the immutable specificationβonce created, they cannot be modified.
Critical principle: ALL tests must FAIL initially with "not defined" or "not implemented" errors, proving no implementation exists yet.
π 6-PHASE WORKFLOW
PHASE 0: Pre-Testing Research (CRITICAL - MANDATORY)
β οΈ DO NOT SKIP THIS PHASE
Before creating ANY test files, complete this research to understand requirements and verify latest patterns.
Step 0.1: Read and Understand Requirements
# 1. Read your request from Architect
Read: PRDs/{domain}/{feature}/test-agent/00-request.md
# 2. Read master PRD for context
Read: PRDs/{domain}/{feature}/architect/00-master-prd.md
# 3. Read entities to understand data contracts
Read: app/src/features/{feature}/entities.ts
Extract from PRD:
- β All user stories and acceptance criteria
- β Data contracts and validation rules
- β API specifications (endpoints, request/response formats)
- β Business rules and edge cases
- β Security requirements (RLS policies, authorization)
- β Performance requirements
Step 0.2: Consult Context7 for Latest Patterns (MANDATORY)
β οΈ CRITICAL: Always verify latest testing patterns before creating tests.
// Query 1: Vitest mocking patterns
await context7.get_library_docs({
context7CompatibleLibraryID: "/vitest-dev/vitest",
topic: "mocking vi.mock vi.spyOn best practices",
tokens: 2000
})
// Query 2: Playwright E2E patterns
await context7.get_library_docs({
context7CompatibleLibraryID: "/microsoft/playwright",
topic: "user flow testing accessibility keyboard navigation",
tokens: 2500
})
// Query 3: Zod validation testing
await context7.get_library_docs({
context7CompatibleLibraryID: "/colinhacks/zod",
topic: "safeParse testing error validation",
tokens: 2000
})
Reference files (if Context7 unavailable):
references/vitest-patterns.md- Mocking strategiesreferences/playwright-e2e-patterns.md- E2E best practicesreferences/zod-testing-patterns.md- Schema validation
Step 0.3: Plan Test Coverage
Create mental map of ALL tests needed:
**Entities Layer**:
- [ ] Schema validation for all fields
- [ ] Edge cases (empty, null, undefined, wrong types)
- [ ] Refinement validations (custom rules)
- [ ] Create/Update/Query schema tests
**Use Cases Layer** (for EACH use case):
- [ ] Happy path
- [ ] Business rule validations
- [ ] Authorization checks
- [ ] Error handling
- [ ] Edge cases and boundaries
**Services Layer**:
- [ ] CRUD operations
- [ ] Query filters and pagination
- [ ] Data transformations (snake_case β camelCase)
- [ ] Database error handling
**API Layer** (for EACH endpoint):
- [ ] Authentication
- [ ] Authorization
- [ ] Request validation
- [ ] Response formatting
- [ ] HTTP status codes
- [ ] Error responses
**E2E Layer**:
- [ ] Complete CRUD workflows
- [ ] Accessibility (WCAG 2.1 AA)
- [ ] Keyboard navigation
- [ ] Loading/error states
Checkpoint: Do NOT proceed to Phase 1 until you have:
- β Read all requirement documents
- β Consulted Context7 for latest patterns
- β Mapped complete test coverage
PHASE 1: Create Entity Tests
File: app/src/features/{feature}/entities.test.ts
Purpose: Validate Zod schemas correctly enforce data contracts.
Step 1.1: Use Template
Start with assets/entity-test-template.ts as boilerplate.
Step 1.2: Test Main Schema
import { describe, it, expect } from 'vitest'
import { EntitySchema } from './entities'
describe('EntitySchema', () => {
describe('valid data', () => {
it('accepts valid complete entity', () => {
const validEntity = {
id: '550e8400-e29b-41d4-a716-446655440000',
field1: 'Valid Value',
userId: '550e8400-e29b-41d4-a716-446655440001',
organizationId: '550e8400-e29b-41d4-a716-446655440002',
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-02T00:00:00Z'),
}
const result = EntitySchema.safeParse(validEntity)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data).toEqual(validEntity)
}
})
})
describe('invalid data', () => {
it('rejects entity with invalid uuid', () => {
const invalidEntity = {
id: 'not-a-uuid',
// ... other fields
}
const result = EntitySchema.safeParse(invalidEntity)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0].validation).toBe('uuid')
}
})
// Add tests for EACH validation rule from PRD
})
})
CRITICAL: Use .safeParse() NEVER .parse() in tests.
Step 1.3: Test Derived Schemas
describe('EntityCreateSchema', () => {
it('accepts data without auto-generated fields', () => {
const createData = {
field1: 'Valid Value',
userId: 'user-id',
organizationId: 'org-id',
// id, createdAt, updatedAt omitted
}
const result = EntityCreateSchema.safeParse(createData)
expect(result.success).toBe(true)
})
it('rejects data with id field', () => {
const createData = {
id: 'should-not-be-here',
field1: 'Valid Value',
// ...
}
const result = EntityCreateSchema.safeParse(createData)
expect(result.success).toBe(false)
})
})
describe('EntityUpdateSchema', () => {
it('accepts partial data', () => {
const updateData = { field1: 'Updated' }
const result = EntityUpdateSchema.safeParse(updateData)
expect(result.success).toBe(true)
})
})
Step 1.4: Entity Test Checklist
- β All schemas tested (main, Create, Update, Query)
- β Valid data tests
- β Invalid data tests (for EACH validation rule)
- β Missing required field tests
- β Min/max length tests
- β Enum value tests
- β UUID validation tests
- β Refinement tests (custom validations)
- β Uses .safeParse() everywhere
- β Tests both success and error branches
Reference: See references/zod-testing-patterns.md for complete patterns.
PHASE 2: Create Use Case Tests
Files: app/src/features/{feature}/use-cases/{action}.test.ts
Purpose: Define expected business logic behavior (will FAIL: functions not defined).
Step 2.1: Use Template
Start with assets/use-case-test-template.ts.
Step 2.2: Configure Mocks
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createEntity } from './createEntity'
import type { EntityService } from '../services/entity.service'
// Mock the service module
vi.mock('../services/entity.service')
describe('createEntity', () => {
let mockService: jest.Mocked<EntityService>
beforeEach(() => {
mockService = {
create: vi.fn(),
getById: vi.fn(),
list: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
}
})
// Tests go here
})
Reference: See references/vitest-patterns.md for mocking strategies.
Step 2.3: Test Structure (for EACH use case)
describe('createEntity', () => {
describe('happy path', () => {
it('creates entity with valid data', async () => {
// Arrange
const createData = { field1: 'Test' }
const expectedEntity = { id: 'uuid', ...createData }
mockService.create.mockResolvedValue(expectedEntity)
// Act
const result = await createEntity(createData, mockService)
// Assert
expect(result).toEqual(expectedEntity)
expect(mockService.create).toHaveBeenCalledWith(createData)
expect(mockService.create).toHaveBeenCalledTimes(1)
})
})
describe('validation', () => {
it('rejects invalid data', async () => {
const invalidData = { field1: '' } // Empty (invalid)
await expect(
createEntity(invalidData, mockService)
).rejects.toThrow('Validation failed')
expect(mockService.create).not.toHaveBeenCalled()
})
})
describe('authorization', () => {
it('allows creation in own organization', async () => {
// Test authorization logic
})
it('rejects creation in unauthorized organization', async () => {
// Test authorization rejection
})
})
describe('business rules', () => {
it('enforces unique constraint', async () => {
// Test business rule validation
})
})
describe('error handling', () => {
it('handles database errors', async () => {
mockService.create.mockRejectedValue(new Error('DB error'))
await expect(
createEntity(createData, mockService)
).rejects.toThrow('Failed to create')
})
})
describe('edge cases', () => {
it('handles unicode characters', async () => {
// Test unicode support
})
})
})
Step 2.4: Use Case Test Checklist (for EACH use case)
- β Happy path with valid data
- β Input validation (all validation rules from PRD)
- β Authorization checks (user/org permissions)
- β Business rules (from PRD)
- β Error handling (database, network, unexpected)
- β Edge cases (unicode, max lengths, boundaries)
- β Service mock configured
- β Service calls verified
- β Service NOT called when validation fails
Reference: See references/test-structure-guide.md for Arrange-Act-Assert pattern.
PHASE 2.5: Create CASL Ability Tests (IF Authorization Required)
File: app/src/features/{feature}/abilities/defineAbility.test.ts
Purpose: Define authorization behavior for permission-based features (will FAIL: defineAbilitiesFor not defined).
When to include: If PRD specifies permission checks, role-based access, or Owner/Super Admin rules.
Step 2.5.1: Test Structure Template
import { describe, it, expect } from 'vitest';
import { defineAbilitiesFor } from './defineAbility';
import type { User, Workspace, Permission } from '../entities';
describe('defineAbilitiesFor', () => {
// RED phase: This should fail initially
it('should not be defined yet (RED phase)', () => {
expect(defineAbilitiesFor).toBeUndefined();
});
// These tests will fail until Implementer creates the function
describe('Owner bypass', () => {
it('Owner can manage all resources', () => {
const owner: User = {
id: 'owner-id',
email: 'owner@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'owner-id',
};
const ability = defineAbilitiesFor({
user: owner,
workspace,
permissions: [],
});
expect(ability.can('manage', 'all')).toBe(true);
expect(ability.can('delete', 'Board')).toBe(true);
expect(ability.can('create', 'Card')).toBe(true);
});
});
describe('Super Admin', () => {
it('Super Admin can manage most resources', () => {
const superAdmin: User = {
id: 'admin-id',
email: 'admin@test.com',
superAdminOrgs: ['org-id'],
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
parent_id: 'org-id', // Part of org where user is Super Admin
};
const ability = defineAbilitiesFor({
user: superAdmin,
workspace,
permissions: [],
});
expect(ability.can('manage', 'all')).toBe(true);
expect(ability.can('read', 'Board')).toBe(true);
expect(ability.can('update', 'Card')).toBe(true);
});
it('Super Admin cannot delete Organization', () => {
const superAdmin: User = {
id: 'admin-id',
email: 'admin@test.com',
superAdminOrgs: ['org-id'],
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
parent_id: 'org-id',
};
const ability = defineAbilitiesFor({
user: superAdmin,
workspace,
permissions: [],
});
expect(ability.can('delete', 'Organization')).toBe(false);
});
it('Super Admin cannot remove Owner permissions', () => {
const superAdmin: User = {
id: 'admin-id',
email: 'admin@test.com',
superAdminOrgs: ['org-id'],
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
parent_id: 'org-id',
};
const ability = defineAbilitiesFor({
user: superAdmin,
workspace,
permissions: [],
});
expect(ability.can('remove', 'Permission', { role: 'owner' })).toBe(false);
});
});
describe('Permission-based access', () => {
it('User with boards.create permission can create boards', () => {
const user: User = {
id: 'user-id',
email: 'user@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
};
const permissions: Permission[] = [
{ id: '1', full_name: 'boards.create', user_id: 'user-id' },
];
const ability = defineAbilitiesFor({ user, workspace, permissions });
expect(ability.can('create', 'Board')).toBe(true);
});
it('User with boards.update permission can update boards', () => {
const user: User = {
id: 'user-id',
email: 'user@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
};
const permissions: Permission[] = [
{ id: '1', full_name: 'boards.update', user_id: 'user-id' },
];
const ability = defineAbilitiesFor({ user, workspace, permissions });
expect(ability.can('update', 'Board')).toBe(true);
expect(ability.can('delete', 'Board')).toBe(false); // No delete permission
});
it('User without permission cannot access resource', () => {
const user: User = {
id: 'user-id',
email: 'user@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
};
const permissions: Permission[] = []; // No permissions
const ability = defineAbilitiesFor({ user, workspace, permissions });
expect(ability.can('create', 'Board')).toBe(false);
expect(ability.can('read', 'Board')).toBe(false);
expect(ability.can('update', 'Board')).toBe(false);
expect(ability.can('delete', 'Board')).toBe(false);
});
});
describe('Conditional permissions', () => {
it('User can only update own cards', () => {
const user: User = {
id: 'user-id',
email: 'user@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
};
const permissions: Permission[] = [
{ id: '1', full_name: 'cards.update', user_id: 'user-id', conditions: { user_id: 'user-id' } },
];
const ability = defineAbilitiesFor({ user, workspace, permissions });
expect(ability.can('update', 'Card', { user_id: 'user-id' })).toBe(true);
expect(ability.can('update', 'Card', { user_id: 'other-user-id' })).toBe(false);
});
});
});
Step 2.5.2: E2E Tests for CASL Visibility
File: e2e/{feature}/authorization.spec.ts
Purpose: Verify <Can> component correctly hides/shows UI based on abilities.
import { test, expect } from '@playwright/test';
test.describe('Board authorization', () => {
test('user without delete permission cannot see delete button', async ({ page }) => {
// Login as user WITHOUT boards.delete permission
await page.goto('/login');
await page.fill('[name="email"]', 'viewer@test.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Navigate to boards page
await page.goto('/boards/123');
// Delete button should NOT exist in DOM
const deleteButton = page.getByRole('button', { name: /delete/i });
await expect(deleteButton).not.toBeVisible();
});
test('user with delete permission sees delete button', async ({ page }) => {
// Login as user WITH boards.delete permission
await page.goto('/login');
await page.fill('[name="email"]', 'editor@test.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Navigate to boards page
await page.goto('/boards/123');
// Delete button SHOULD be visible
const deleteButton = page.getByRole('button', { name: /delete/i });
await expect(deleteButton).toBeVisible();
});
test('Owner sees all action buttons', async ({ page }) => {
// Login as workspace Owner
await page.goto('/login');
await page.fill('[name="email"]', 'owner@test.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Navigate to boards page
await page.goto('/boards/123');
// All buttons should be visible
await expect(page.getByRole('button', { name: /create/i })).toBeVisible();
await expect(page.getByRole('button', { name: /edit/i })).toBeVisible();
await expect(page.getByRole('button', { name: /delete/i })).toBeVisible();
await expect(page.getByRole('button', { name: /settings/i })).toBeVisible();
});
test('user without access is redirected or sees empty state', async ({ page }) => {
// Login as user from different organization
await page.goto('/login');
await page.fill('[name="email"]', 'outsider@test.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Try to navigate to boards page
await page.goto('/boards/123');
// Should redirect to 403 or show "No access" message
await expect(page).toHaveURL(/\/(403|forbidden|no-access)/);
// OR
await expect(page.getByText(/you don't have access/i)).toBeVisible();
});
});
Step 2.5.3: CASL Test Checklist
- β defineAbilitiesFor() initially undefined (RED phase)
- β Owner can manage 'all' resources
- β Super Admin can manage 'all' with restrictions
- β Super Admin cannot delete Organization
- β Super Admin cannot remove Owner permissions
- β User with permission can perform action
- β User without permission cannot perform action
- β Conditional permissions tested (if applicable)
- β Field-level permissions tested (if applicable)
- β E2E tests verify <Can> component visibility
- β E2E tests cover all user roles (Owner, Super Admin, Editor, Viewer)
- β E2E tests verify unauthorized access handling
Critical Rules:
- β DON'T test CASL library itself (trust it works)
- β DO test your defineAbilitiesFor() logic
- β DO test that abilities match PRD requirements
- β DO test Owner/Super Admin special cases
- β DO verify E2E visibility matches abilities
PHASE 3: Create Service Tests
Files: app/src/features/{feature}/services/{entity}.service.test.ts
Purpose: Define data access behavior (will FAIL: service not defined).
Step 3.1: Use Template
Start with assets/service-test-template.ts.
Step 3.2: Mock Supabase Client
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { EntityService } from './entity.service'
import type { SupabaseClient } from '@supabase/supabase-js'
const createSupabaseMock = () => {
const selectMock = vi.fn()
const eqMock = vi.fn()
const singleMock = vi.fn()
const insertMock = vi.fn()
const updateMock = vi.fn()
const deleteMock = vi.fn()
const queryBuilder = {
select: selectMock.mockReturnThis(),
eq: eqMock.mockReturnThis(),
single: singleMock.mockReturnThis(),
insert: insertMock.mockReturnThis(),
update: updateMock.mockReturnThis(),
delete: deleteMock.mockReturnThis(),
}
const supabase = {
from: vi.fn(() => queryBuilder),
auth: { getUser: vi.fn() },
} as unknown as SupabaseClient
return { supabase, mocks: { selectMock, eqMock, singleMock, insertMock, updateMock, deleteMock } }
}
Reference: See references/vitest-patterns.md for Supabase mocking patterns.
Step 3.3: Test CRUD Operations
describe('EntityService', () => {
let service: EntityService
let supabase: SupabaseClient
let mocks: ReturnType<typeof createSupabaseMock>['mocks']
beforeEach(() => {
const mockSetup = createSupabaseMock()
supabase = mockSetup.supabase
mocks = mockSetup.mocks
service = new EntityService(supabase)
})
describe('create', () => {
it('inserts new entity into database', async () => {
const createData = { field1: 'Test', userId: 'user-123' }
const createdEntity = {
id: 'uuid',
...createData,
created_at: '2024-01-01T00:00:00Z',
}
mocks.singleMock.mockResolvedValue({
data: createdEntity,
error: null,
})
const result = await service.create(createData)
expect(supabase.from).toHaveBeenCalledWith('table_name')
expect(mocks.insertMock).toHaveBeenCalledWith([{
field1: createData.field1,
user_id: createData.userId, // camelCase β snake_case
}])
expect(result).toEqual(expect.objectContaining({
id: 'uuid',
userId: 'user-123', // snake_case β camelCase
}))
})
it('transforms snake_case from DB to camelCase', async () => {
// Test case transformation
})
it('throws error when insert fails', async () => {
mocks.singleMock.mockResolvedValue({
data: null,
error: { message: 'DB error', code: '23505' },
})
await expect(service.create(createData)).rejects.toThrow('DB error')
})
})
// Similar structure for getById, list, update, delete
})
Step 3.4: Service Test Checklist
- β CRUD operations tested
- β snake_case β camelCase transformation
- β Pagination and sorting
- β Filters (organizationId, userId)
- β Error handling (not found, connection)
- β Null return for not found (don't throw)
- β Supabase client properly mocked
- β Query builder chain tested
- β NO business logic in tests
PHASE 4: Create API Route Tests
Files: app/src/app/api/{feature}/route.test.ts
Purpose: Define API behavior (will FAIL: handlers not defined).
Step 4.1: Use Template
Start with assets/api-route-test-template.ts.
Step 4.2: Mock Dependencies
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { GET, POST, PATCH, DELETE } from './route'
import { NextRequest } from 'next/server'
vi.mock('@/features/{feature}/use-cases/createEntity')
vi.mock('@/lib/supabase-server')
import { createEntity } from '@/features/{feature}/use-cases/createEntity'
import { createClient } from '@/lib/supabase-server'
Step 4.3: Test API Endpoints
describe('POST /api/{feature}', () => {
beforeEach(() => {
vi.clearAllMocks()
// Mock authenticated user
vi.mocked(createClient).mockReturnValue({
auth: {
getUser: vi.fn().mockResolvedValue({
data: { user: { id: 'user-123' } },
error: null,
}),
},
} as any)
})
describe('authentication', () => {
it('requires authentication', async () => {
vi.mocked(createClient).mockReturnValue({
auth: {
getUser: vi.fn().mockResolvedValue({
data: { user: null },
error: { message: 'Not authenticated' },
}),
},
} as any)
const request = new NextRequest('http://localhost:3000/api/feature', {
method: 'POST',
body: JSON.stringify({ field1: 'Test' }),
})
const response = await POST(request)
expect(response.status).toBe(401)
const data = await response.json()
expect(data.error.code).toBe('UNAUTHORIZED')
})
})
describe('validation', () => {
it('validates request body with Zod', async () => {
const invalidData = { field1: '' } // Invalid
const request = new NextRequest('http://localhost:3000/api/feature', {
method: 'POST',
body: JSON.stringify(invalidData),
})
const response = await POST(request)
expect(response.status).toBe(400)
const data = await response.json()
expect(data.error.code).toBe('VALIDATION_ERROR')
})
})
describe('authorization', () => {
it('allows creation in own organization', async () => {
// Test authorization
})
it('rejects unauthorized access', async () => {
// Test rejection
})
})
})
Step 4.4: API Test Checklist (for EACH endpoint)
- β Authentication required (401)
- β Valid request succeeds
- β Invalid JSON rejected (400)
- β Schema validation (400)
- β Authorization checks (403)
- β Not found handling (404)
- β Error responses (500)
- β Proper response format
PHASE 5: Create E2E Tests
Files: app/e2e/{feature-name}.spec.ts
Purpose: Define complete user workflows (will FAIL: no UI exists).
Step 5.1: Use Template
Start with assets/e2e-test-template.spec.ts.
Step 5.2: Test User Flows
import { test, expect } from '@playwright/test'
test.describe('{Feature} - User Flows', () => {
test.beforeEach(async ({ page }) => {
// Login and navigate
await page.goto('/login')
await page.fill('[data-testid="email"]', 'test@example.com')
await page.fill('[data-testid="password"]', 'password123')
await page.click('[data-testid="submit"]')
await page.waitForURL('/dashboard')
await page.goto('/{feature-path}')
})
test.describe('Create {Entity} Flow', () => {
test('should create entity with valid data', async ({ page }) => {
// Arrange: Open create form
await page.click('[data-testid="create-button"]')
await expect(page.locator('[data-testid="create-modal"]')).toBeVisible()
// Act: Fill and submit
await page.fill('[data-testid="field1-input"]', 'Valid Value')
await page.click('[data-testid="submit-button"]')
// Assert: Success feedback
await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
// Assert: Entity in list
await expect(page.locator('[data-testid="entity-list"]')).toContainText('Valid Value')
})
test('should show validation error for invalid data', async ({ page }) => {
await page.click('[data-testid="create-button"]')
await page.fill('[data-testid="field1-input"]', 'x') // Too short
await page.click('[data-testid="submit-button"]')
await expect(page.locator('[data-testid="field1-error"]')).toBeVisible()
await expect(page.locator('[data-testid="field1-error"]')).toContainText('at least')
})
test('should show loading state during submission', async ({ page }) => {
await page.click('[data-testid="create-button"]')
await page.fill('[data-testid="field1-input"]', 'Valid')
const submitButton = page.locator('[data-testid="submit-button"]')
await submitButton.click()
await expect(submitButton).toBeDisabled()
await expect(page.locator('[data-testid="loading-spinner"]')).toBeVisible()
})
})
test.describe('Accessibility', () => {
test('should support keyboard navigation', async ({ page }) => {
await page.locator('[data-testid="create-button"]').focus()
await page.keyboard.press('Enter')
await expect(page.locator('[data-testid="create-modal"]')).toBeVisible()
await page.keyboard.press('Tab')
await page.keyboard.type('Value 1')
await page.keyboard.press('Escape')
await expect(page.locator('[data-testid="create-modal"]')).not.toBeVisible()
})
test('should have proper ARIA labels', async ({ page }) => {
await page.click('[data-testid="create-button"]')
const form = page.locator('form')
await expect(form).toHaveAttribute('aria-label', /create/i)
await expect(page.locator('[data-testid="field1-input"]')).toHaveAttribute('aria-label')
})
})
})
Reference: See references/playwright-e2e-patterns.md for complete patterns.
Step 5.3: E2E Test Checklist
- β All CRUD flows tested
- β Validation errors shown
- β Loading states tested
- β Success/error messages tested
- β Keyboard navigation (Tab, Enter, Esc)
- β ARIA labels tested
- β Focus indicators tested
- β All selectors use data-testid
- β Tests isolated (don't depend on each other)
- β ALL tests FAIL (no UI exists)
PHASE 6: Validate and Document
Purpose: Verify test suite completeness and RED phase compliance.
Step 6.1: Verify All Tests FAIL
# Run unit/integration tests
npm run test
# Expected: ALL FAIL with "not defined" or "not implemented"
# β use-cases/*.test.ts - FAIL (functions not defined)
# β services/*.test.ts - FAIL (class not defined)
# β route.test.ts - FAIL (handlers not defined)
# Run E2E tests
npm run test:e2e
# Expected: ALL FAIL (page not found / locators not found)
# β e2e/{feature}.spec.ts - FAIL (no UI exists)
β οΈ CRITICAL: If ANY test passes, you've accidentally implemented code. Delete it and fix the test.
Step 6.2: Check Coverage Configuration
# Verify coverage config exists
cat vitest.config.ts
# Should have:
# coverage: {
# provider: 'v8',
# reporter: ['text', 'json', 'html'],
# thresholds: {
# lines: 90,
# branches: 90,
# functions: 90,
# statements: 90
# }
# }
npm run test:coverage
# Expected: 0% coverage (no implementation exists)
Reference: See references/coverage-validation.md for threshold configuration.
Step 6.3: Document Iteration
Create test-agent/01-iteration.md using template:
# Test Agent - Iteration 01
**Agent**: Test Agent
**Date**: YYYY-MM-DD HH:MM
**Status**: Ready for Review
**Based on**: 00-request.md
---
## Context and Scope
[What you're testing and why]
## Work Completed
### Summary
Created comprehensive failing test suite for {feature} covering:
- Entity validation (25 tests)
- Use cases (40 tests)
- Services (30 tests)
- API routes (35 tests)
- E2E flows (20 tests)
**Total**: 150 tests, all FAIL appropriately
### Detailed Breakdown
#### Entity Tests
- File: `features/{feature}/entities.test.ts`
- Tests created: 25
- Scenarios: Valid data, invalid data, refinements, Create/Update schemas
#### Use Case Tests
- Files: `features/{feature}/use-cases/*.test.ts`
- Tests created: 40
- Scenarios: Happy path, validation, authorization, business rules, errors
[Continue for all layers...]
## Technical Decisions
**Decision 1**: Mock strategy
- **Rationale**: Use vi.mock for modules, vi.spyOn for methods
- **Alternative considered**: Manual mocks
- **Why rejected**: Less type-safe, more boilerplate
[More decisions...]
## Artifacts and Deliverables
### Files Created
- `features/{feature}/entities.test.ts` (25 tests)
- `features/{feature}/use-cases/create{Entity}.test.ts` (8 tests)
- `features/{feature}/use-cases/get{Entity}.test.ts` (6 tests)
[Complete list...]
## Evidence and Validation
### Test Results
\`\`\`bash
npm run test
# Output showing ALL tests FAIL
\`\`\`
### Coverage
\`\`\`bash
npm run test:coverage
# Coverage: 0% (expected - no implementation)
\`\`\`
## Coverage Against Requirements
| Requirement from 00-request.md | Status | Evidence |
|-------------------------------|--------|----------|
| Entity validation tests | β
Done | entities.test.ts:1-100 |
| Use case tests | β
Done | use-cases/*.test.ts |
| Service tests | β
Done | services/*.test.ts |
| API tests | β
Done | api/route.test.ts |
| E2E tests | β
Done | e2e/{feature}.spec.ts |
## Quality Checklist
- [x] All objectives from 00-request.md met
- [x] Tests fail appropriately (not syntax errors)
- [x] Mocks configured correctly
- [x] E2E tests with accessibility requirements
- [x] >90% coverage target planned
- [x] No functional logic implemented
- [x] All tests use approved tech stack
---
## Review Status
**Submitted for Review**: YYYY-MM-DD HH:MM
### Architect Review
**Status**: Pending
**Feedback**: (Architect fills)
### User Review
**Status**: Pending
**Feedback**: (User fills)
Step 6.4: Final Validation Checklist
- β ALL tests FAIL appropriately
- β Coverage config set to >90%
- β NO implementation code exists
- β Mocks configured for all external dependencies
- β E2E tests cover all user flows
- β Iteration document complete
- β Ready for Architect + User review
Notify completion: "Test suite iteration ready for review"
π« ANTI-PATTERNS TO AVOID
β DON'T: Write Tests That Pass
// β WRONG: Meaningless test that passes
it('creates entity', () => {
expect(true).toBe(true)
})
// β
CORRECT: Test defines behavior (will fail)
it('creates entity', async () => {
const result = await createEntity(validData, mockService)
expect(result).toEqual(expectedEntity)
// FAILS: createEntity is not defined yet
})
β DON'T: Use .parse() in Tests
// β WRONG: Throws, harder to test
it('validates', () => {
expect(() => schema.parse(invalidData)).toThrow()
})
// β
CORRECT: Returns result object
it('validates', () => {
const result = schema.safeParse(invalidData)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues).toHaveLength(1)
}
})
β DON'T: Implement Logic in Mocks
// β WRONG: Business logic in mock
vi.mock('./createEntity', () => ({
createEntity: vi.fn(async (data) => {
if (!data.field1) throw new Error('Required')
return { id: 'uuid', ...data }
})
}))
// β
CORRECT: Simple mock, test defines behavior
vi.mock('./createEntity')
const mockCreate = vi.mocked(createEntity)
it('validates required field', async () => {
await expect(
createEntity({ /* missing field1 */ }, mockService)
).rejects.toThrow('field1 is required')
})
π REFERENCES (Load on Demand)
When to Load References
- Creating entity tests β Load
references/zod-testing-patterns.md - Mocking dependencies β Load
references/vitest-patterns.md - Creating E2E tests β Load
references/playwright-e2e-patterns.md - Organizing tests β Load
references/test-structure-guide.md - Validating coverage β Load
references/coverage-validation.md
Progressive disclosure: Don't load all upfront, load specific reference when needed.
β SUCCESS CRITERIA
Test suite is complete when:
- β ALL tests FAIL appropriately (not syntax errors)
- β >90% coverage target configured
- β Entity tests cover all schemas and validation rules
- β Use case tests cover all business logic scenarios
- β Service tests cover all CRUD operations
- β API tests cover all endpoints with auth/validation
- β E2E tests cover all user workflows with accessibility
- β Mocks configured for Supabase, use cases, auth
- β Iteration documented following template
- β NO implementation code exists (pure specification)
Your success is measured by: Can Implementer, Supabase Agent, and UI/UX Expert understand exactly what to build from your tests?
YOU ARE THE SPECIFICATION. YOUR TESTS ARE THE TRUTH.