Initial project setup: monorepo structure and documentation
This commit is contained in:
716
docs/TESTS.md
Normal file
716
docs/TESTS.md
Normal file
@@ -0,0 +1,716 @@
|
||||
# Testing Strategy
|
||||
|
||||
## Overview
|
||||
|
||||
GridPilot employs a comprehensive BDD (Behavior-Driven Development) testing strategy across three distinct layers: **Unit**, **Integration**, and **End-to-End (E2E)**. Each layer validates different aspects of the system while maintaining a consistent Given/When/Then approach that emphasizes behavior over implementation.
|
||||
|
||||
This document provides practical guidance on testing philosophy, test organization, tooling, and execution patterns for GridPilot.
|
||||
|
||||
---
|
||||
|
||||
## BDD Philosophy
|
||||
|
||||
### Why BDD for GridPilot?
|
||||
|
||||
GridPilot manages complex business rules around league management, team registration, event scheduling, result processing, and standings calculation. These rules must be:
|
||||
|
||||
- **Understandable** by non-technical stakeholders (league admins, race organizers)
|
||||
- **Verifiable** through automated tests that mirror real-world scenarios
|
||||
- **Maintainable** as business requirements evolve
|
||||
|
||||
BDD provides a shared vocabulary (Given/When/Then) that bridges the gap between domain experts and developers, ensuring tests document expected behavior rather than technical implementation details.
|
||||
|
||||
### Given/When/Then Format
|
||||
|
||||
All tests—regardless of layer—follow this structure:
|
||||
|
||||
```typescript
|
||||
// Given: Establish initial state/context
|
||||
// When: Perform the action being tested
|
||||
// Then: Assert the expected outcome
|
||||
```
|
||||
|
||||
**Example (Unit Test):**
|
||||
```typescript
|
||||
describe('League Domain Entity', () => {
|
||||
it('should add a team when team limit not reached', () => {
|
||||
// Given
|
||||
const league = new League('Summer Series', { maxTeams: 10 });
|
||||
const team = new Team('Racing Legends');
|
||||
|
||||
// When
|
||||
const result = league.addTeam(team);
|
||||
|
||||
// Then
|
||||
expect(result.isSuccess()).toBe(true);
|
||||
expect(league.teams).toContain(team);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
This pattern applies equally to integration tests (with real database operations) and E2E tests (with full UI workflows).
|
||||
|
||||
---
|
||||
|
||||
## Test Types & Organization
|
||||
|
||||
### Unit Tests (`/tests/unit`)
|
||||
|
||||
**Scope:** Domain entities, value objects, and application use cases with mocked ports (repositories, external services).
|
||||
|
||||
**Tooling:** Vitest (fast, TypeScript-native, ESM support)
|
||||
|
||||
**Execution:** Parallel, target <1 second total runtime
|
||||
|
||||
**Purpose:**
|
||||
- Validate business logic in isolation
|
||||
- Ensure domain invariants hold (e.g., team limits, scoring rules)
|
||||
- Test use case orchestration with mocked dependencies
|
||||
|
||||
**Examples from Architecture:**
|
||||
|
||||
1. **Domain Entity Test:**
|
||||
```typescript
|
||||
// League.addTeam() validation
|
||||
Given a League with maxTeams=10 and 9 current teams
|
||||
When addTeam() is called with a valid Team
|
||||
Then the team is added successfully
|
||||
|
||||
Given a League with maxTeams=10 and 10 current teams
|
||||
When addTeam() is called
|
||||
Then a DomainError is returned with "Team limit reached"
|
||||
```
|
||||
|
||||
2. **Use Case Test:**
|
||||
```typescript
|
||||
// GenerateStandingsUseCase
|
||||
Given a League with 5 teams and completed races
|
||||
When execute() is called
|
||||
Then LeagueRepository.findById() is invoked
|
||||
And ScoringRule.calculatePoints() is called for each team
|
||||
And sorted standings are returned
|
||||
```
|
||||
|
||||
3. **Scoring Rule Test:**
|
||||
```typescript
|
||||
// ScoringRule.calculatePoints()
|
||||
Given a F1-style scoring rule (25-18-15-12-10-8-6-4-2-1)
|
||||
When calculatePoints(position=1) is called
|
||||
Then 25 points are returned
|
||||
|
||||
Given the same rule
|
||||
When calculatePoints(position=11) is called
|
||||
Then 0 points are returned
|
||||
```
|
||||
|
||||
**Key Practices:**
|
||||
- Mock only at architecture boundaries (ports like `ILeagueRepository`)
|
||||
- Never mock domain entities or value objects
|
||||
- Keep tests fast (<10ms per test)
|
||||
- Use in-memory test doubles for simple cases
|
||||
|
||||
---
|
||||
|
||||
### Integration Tests (`/tests/integration`)
|
||||
|
||||
**Scope:** Repository implementations, infrastructure adapters (PostgreSQL, Redis, OAuth clients, result importers).
|
||||
|
||||
**Tooling:** Vitest + Testcontainers (spins up real PostgreSQL/Redis in Docker)
|
||||
|
||||
**Execution:** Sequential, ~10 seconds per suite
|
||||
|
||||
**Purpose:**
|
||||
- Validate that infrastructure adapters correctly implement port interfaces
|
||||
- Test database queries, migrations, and transaction handling
|
||||
- Ensure external API clients handle authentication and error scenarios
|
||||
|
||||
**Examples from Architecture:**
|
||||
|
||||
1. **Repository Test:**
|
||||
```typescript
|
||||
// PostgresLeagueRepository
|
||||
Given a PostgreSQL container is running
|
||||
When save() is called with a League entity
|
||||
Then the league is persisted to the database
|
||||
And findById() returns the same league with correct attributes
|
||||
```
|
||||
|
||||
2. **OAuth Client Test:**
|
||||
```typescript
|
||||
// IRacingOAuthClient
|
||||
Given valid iRacing credentials
|
||||
When authenticate() is called
|
||||
Then an access token is returned
|
||||
And the token is cached in Redis for 1 hour
|
||||
|
||||
Given expired credentials
|
||||
When authenticate() is called
|
||||
Then an AuthenticationError is thrown
|
||||
```
|
||||
|
||||
3. **Result Importer Test:**
|
||||
```typescript
|
||||
// EventResultImporter
|
||||
Given an Event exists in the database
|
||||
When importResults() is called with iRacing session data
|
||||
Then Driver entities are created/updated
|
||||
And EventResult entities are persisted with correct positions/times
|
||||
And the Event status is updated to 'COMPLETED'
|
||||
```
|
||||
|
||||
**Key Practices:**
|
||||
- Use Testcontainers to spin up real databases (not mocks)
|
||||
- Clean database state between tests (truncate tables or use transactions)
|
||||
- Seed minimal test data via SQL fixtures
|
||||
- Test both success and failure paths (network errors, constraint violations)
|
||||
|
||||
---
|
||||
|
||||
### End-to-End Tests (`/tests/e2e`)
|
||||
|
||||
**Scope:** Full user workflows spanning web-client → web-api → database.
|
||||
|
||||
**Tooling:** Playwright + Docker Compose (orchestrates all services)
|
||||
|
||||
**Execution:** ~2 minutes per scenario
|
||||
|
||||
**Purpose:**
|
||||
- Validate complete user journeys from UI interactions to database changes
|
||||
- Ensure services integrate correctly in a production-like environment
|
||||
- Catch regressions in multi-service workflows
|
||||
|
||||
**Examples from Architecture:**
|
||||
|
||||
1. **League Creation Workflow:**
|
||||
```gherkin
|
||||
Given an authenticated league admin
|
||||
When they navigate to "Create League"
|
||||
And fill in league name, scoring system, and team limit
|
||||
And submit the form
|
||||
Then the league appears in the admin dashboard
|
||||
And the database contains the new league record
|
||||
And the league is visible to other users
|
||||
```
|
||||
|
||||
2. **Team Registration Workflow:**
|
||||
```gherkin
|
||||
Given a published league with 5/10 team slots filled
|
||||
When a team captain navigates to the league page
|
||||
And clicks "Join League"
|
||||
And fills in team name and roster
|
||||
And submits the form
|
||||
Then the team appears in the league's team list
|
||||
And the team count updates to 6/10
|
||||
And the captain receives a confirmation email
|
||||
```
|
||||
|
||||
3. **Automated Result Import:**
|
||||
```gherkin
|
||||
Given a League with an upcoming Event
|
||||
And iRacing OAuth credentials are configured
|
||||
When the scheduled import job runs
|
||||
Then the job authenticates with iRacing
|
||||
And fetches session results for the Event
|
||||
And creates EventResult records in the database
|
||||
And updates the Event status to 'COMPLETED'
|
||||
And triggers standings recalculation
|
||||
```
|
||||
|
||||
4. **Companion App Login Automation:**
|
||||
```gherkin
|
||||
Given a League Admin enables companion app login automation
|
||||
When the companion app is launched
|
||||
Then the app polls for a generated login token from web-api
|
||||
And auto-fills iRacing credentials from the admin's profile
|
||||
And logs into iRacing automatically
|
||||
And confirms successful login to web-api
|
||||
```
|
||||
|
||||
**Key Practices:**
|
||||
- Use Playwright's Page Object pattern for reusable UI interactions
|
||||
- Test both happy paths and error scenarios (validation errors, network failures)
|
||||
- Clean database state between scenarios (via API or direct SQL)
|
||||
- Run E2E tests in CI before merging to main branch
|
||||
|
||||
---
|
||||
|
||||
## Test Data Strategy
|
||||
|
||||
### Fixtures & Seeding
|
||||
|
||||
**Unit Tests:**
|
||||
- Use in-memory domain objects (no database)
|
||||
- Factory functions for common test entities:
|
||||
```typescript
|
||||
function createTestLeague(overrides?: Partial<LeagueProps>): League {
|
||||
return new League('Test League', { maxTeams: 10, ...overrides });
|
||||
}
|
||||
```
|
||||
|
||||
**Integration Tests:**
|
||||
- Use Testcontainers to spin up fresh PostgreSQL instances
|
||||
- Seed minimal test data via SQL scripts:
|
||||
```sql
|
||||
-- tests/integration/fixtures/leagues.sql
|
||||
INSERT INTO leagues (id, name, max_teams) VALUES
|
||||
('league-1', 'Test League', 10);
|
||||
```
|
||||
- Clean state between tests (truncate tables or rollback transactions)
|
||||
|
||||
**E2E Tests:**
|
||||
- Pre-seed database via migrations before Docker Compose starts
|
||||
- Use API endpoints to create test data when possible (validates API behavior)
|
||||
- Database cleanup between scenarios:
|
||||
```typescript
|
||||
// tests/e2e/support/database.ts
|
||||
export async function cleanDatabase() {
|
||||
await sql`TRUNCATE TABLE event_results CASCADE`;
|
||||
await sql`TRUNCATE TABLE events CASCADE`;
|
||||
await sql`TRUNCATE TABLE teams CASCADE`;
|
||||
await sql`TRUNCATE TABLE leagues CASCADE`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker E2E Setup
|
||||
|
||||
### Architecture
|
||||
|
||||
E2E tests run against a full stack orchestrated by `docker-compose.test.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_DB: gridpilot_test
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
|
||||
web-api:
|
||||
build: ./src/apps/web-api
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
environment:
|
||||
DATABASE_URL: postgres://test:test@postgres:5432/gridpilot_test
|
||||
REDIS_URL: redis://redis:6379
|
||||
ports:
|
||||
- "3000:3000"
|
||||
```
|
||||
|
||||
### Execution Flow
|
||||
|
||||
1. **Start Services:** `docker compose -f docker-compose.test.yml up -d`
|
||||
2. **Run Migrations:** `npm run migrate:test` (seeds database)
|
||||
3. **Execute Tests:** Playwright targets `http://localhost:3000`
|
||||
4. **Teardown:** `docker compose -f docker-compose.test.yml down -v`
|
||||
|
||||
### Environment Setup
|
||||
|
||||
```bash
|
||||
# tests/e2e/setup.ts
|
||||
export async function globalSetup() {
|
||||
// Wait for web-api to be ready
|
||||
await waitForService('http://localhost:3000/health');
|
||||
|
||||
// Run database migrations
|
||||
await runMigrations();
|
||||
}
|
||||
|
||||
export async function globalTeardown() {
|
||||
// Stop Docker Compose services
|
||||
await exec('docker compose -f docker-compose.test.yml down -v');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BDD Scenario Examples
|
||||
|
||||
### 1. League Creation (Success + Failure)
|
||||
|
||||
```gherkin
|
||||
Scenario: Admin creates a new league
|
||||
Given an authenticated admin user
|
||||
When they submit a league form with:
|
||||
| name | Summer Series 2024 |
|
||||
| maxTeams | 12 |
|
||||
| scoringSystem | F1 |
|
||||
Then the league is created successfully
|
||||
And the admin is redirected to the league dashboard
|
||||
And the database contains the new league
|
||||
|
||||
Scenario: League creation fails with duplicate name
|
||||
Given a league named "Summer Series 2024" already exists
|
||||
When an admin submits a league form with name "Summer Series 2024"
|
||||
Then the form displays error "League name already exists"
|
||||
And no new league is created in the database
|
||||
```
|
||||
|
||||
### 2. Team Registration (Success + Failure)
|
||||
|
||||
```gherkin
|
||||
Scenario: Team registers for a league
|
||||
Given a published league with 5/10 team slots
|
||||
When a team captain submits registration with:
|
||||
| teamName | Racing Legends |
|
||||
| drivers | Alice, Bob, Carol |
|
||||
Then the team is added to the league
|
||||
And the team count updates to 6/10
|
||||
And the captain receives a confirmation email
|
||||
|
||||
Scenario: Registration fails when league is full
|
||||
Given a published league with 10/10 team slots
|
||||
When a team captain attempts to register
|
||||
Then the form displays error "League is full"
|
||||
And the team is not added to the league
|
||||
```
|
||||
|
||||
### 3. Automated Result Import (Success + Failure)
|
||||
|
||||
```gherkin
|
||||
Scenario: Import results from iRacing
|
||||
Given a League with an Event scheduled for today
|
||||
And iRacing OAuth credentials are configured
|
||||
When the scheduled import job runs
|
||||
Then the job authenticates with iRacing API
|
||||
And fetches session results for the Event
|
||||
And creates EventResult records for each driver
|
||||
And updates the Event status to 'COMPLETED'
|
||||
And triggers standings recalculation
|
||||
|
||||
Scenario: Import fails with invalid credentials
|
||||
Given an Event with expired iRacing credentials
|
||||
When the import job runs
|
||||
Then an AuthenticationError is logged
|
||||
And the Event status remains 'SCHEDULED'
|
||||
And an admin notification is sent
|
||||
```
|
||||
|
||||
### 4. Parallel Scoring Calculation
|
||||
|
||||
```gherkin
|
||||
Scenario: Calculate standings for multiple leagues concurrently
|
||||
Given 5 active leagues with completed events
|
||||
When the standings recalculation job runs
|
||||
Then each league's standings are calculated in parallel
|
||||
And the process completes in <5 seconds
|
||||
And all standings are persisted correctly
|
||||
And no race conditions occur (validated via database integrity checks)
|
||||
```
|
||||
|
||||
### 5. Companion App Login Automation
|
||||
|
||||
```gherkin
|
||||
Scenario: Companion app logs into iRacing automatically
|
||||
Given a League Admin enables companion app login automation
|
||||
And provides their iRacing credentials
|
||||
When the companion app is launched
|
||||
Then the app polls web-api for a login token
|
||||
And retrieves the admin's encrypted credentials
|
||||
And auto-fills the iRacing login form
|
||||
And submits the login request
|
||||
And confirms successful login to web-api
|
||||
And caches the session token for 24 hours
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Coverage Goals
|
||||
|
||||
### Target Coverage Levels
|
||||
|
||||
- **Domain/Application Layers:** >90% (critical business logic)
|
||||
- **Infrastructure Layer:** >80% (repository implementations, adapters)
|
||||
- **Presentation Layer:** Smoke tests (basic rendering, no exhaustive UI coverage)
|
||||
|
||||
### Running Coverage Reports
|
||||
|
||||
```bash
|
||||
# Unit + Integration coverage
|
||||
npm run test:coverage
|
||||
|
||||
# View HTML report
|
||||
open coverage/index.html
|
||||
|
||||
# E2E coverage (via Istanbul)
|
||||
npm run test:e2e:coverage
|
||||
```
|
||||
|
||||
### What to Prioritize
|
||||
|
||||
1. **Domain Entities:** Invariants, validation rules, state transitions
|
||||
2. **Use Cases:** Orchestration logic, error handling, port interactions
|
||||
3. **Repositories:** CRUD operations, query builders, transaction handling
|
||||
4. **Adapters:** External API clients, OAuth flows, result importers
|
||||
|
||||
**What NOT to prioritize:**
|
||||
- Trivial getters/setters
|
||||
- Framework boilerplate (Express route handlers)
|
||||
- UI styling (covered by visual regression tests if needed)
|
||||
|
||||
---
|
||||
|
||||
## Continuous Testing
|
||||
|
||||
### Watch Mode (Development)
|
||||
|
||||
```bash
|
||||
# Auto-run unit tests on file changes
|
||||
npm run test:watch
|
||||
|
||||
# Auto-run integration tests (slower, but useful for DB work)
|
||||
npm run test:integration:watch
|
||||
```
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Code Push] --> B[Unit Tests]
|
||||
B --> C[Integration Tests]
|
||||
C --> D[E2E Tests]
|
||||
D --> E[Deploy to Staging]
|
||||
```
|
||||
|
||||
**Execution Order:**
|
||||
1. **Unit Tests** (parallel, <1 second) — fail fast on logic errors
|
||||
2. **Integration Tests** (sequential, ~10 seconds) — catch infrastructure issues
|
||||
3. **E2E Tests** (sequential, ~2 minutes) — validate full workflows
|
||||
4. **Deploy** — only if all tests pass
|
||||
|
||||
**Parallelization:**
|
||||
- Unit tests run in parallel (Vitest default)
|
||||
- Integration tests run sequentially (avoid database conflicts)
|
||||
- E2E tests run sequentially (UI interactions are stateful)
|
||||
|
||||
---
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### 1. Test Behavior, Not Implementation
|
||||
|
||||
**❌ Bad (overfitted to implementation):**
|
||||
```typescript
|
||||
it('should call repository.save() once', () => {
|
||||
const repo = mock<ILeagueRepository>();
|
||||
const useCase = new CreateLeagueUseCase(repo);
|
||||
useCase.execute({ name: 'Test' });
|
||||
expect(repo.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Good (tests observable behavior):**
|
||||
```typescript
|
||||
it('should persist the league to the repository', async () => {
|
||||
const repo = new InMemoryLeagueRepository();
|
||||
const useCase = new CreateLeagueUseCase(repo);
|
||||
|
||||
const result = await useCase.execute({ name: 'Test' });
|
||||
|
||||
expect(result.isSuccess()).toBe(true);
|
||||
const league = await repo.findById(result.value.id);
|
||||
expect(league?.name).toBe('Test');
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Mock Only at Architecture Boundaries
|
||||
|
||||
**Ports (interfaces)** should be mocked in use case tests:
|
||||
```typescript
|
||||
const mockRepo = mock<ILeagueRepository>({
|
||||
save: jest.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
```
|
||||
|
||||
**Domain entities** should NEVER be mocked:
|
||||
```typescript
|
||||
// ❌ Don't do this
|
||||
const mockLeague = mock<League>();
|
||||
|
||||
// ✅ Do this
|
||||
const league = new League('Test League', { maxTeams: 10 });
|
||||
```
|
||||
|
||||
### 3. Keep Tests Readable and Maintainable
|
||||
|
||||
**Arrange-Act-Assert Pattern:**
|
||||
```typescript
|
||||
it('should calculate standings correctly', () => {
|
||||
// Arrange: Set up test data
|
||||
const league = createTestLeague();
|
||||
const teams = [createTestTeam('Team A'), createTestTeam('Team B')];
|
||||
const results = [createTestResult(teams[0], position: 1)];
|
||||
|
||||
// Act: Perform the action
|
||||
const standings = league.calculateStandings(results);
|
||||
|
||||
// Assert: Verify the outcome
|
||||
expect(standings[0].team).toBe(teams[0]);
|
||||
expect(standings[0].points).toBe(25);
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Test Error Scenarios
|
||||
|
||||
Don't just test the happy path:
|
||||
```typescript
|
||||
describe('League.addTeam()', () => {
|
||||
it('should add team successfully', () => { /* ... */ });
|
||||
|
||||
it('should fail when team limit reached', () => {
|
||||
const league = createTestLeague({ maxTeams: 1 });
|
||||
league.addTeam(createTestTeam('Team A'));
|
||||
|
||||
const result = league.addTeam(createTestTeam('Team B'));
|
||||
|
||||
expect(result.isFailure()).toBe(true);
|
||||
expect(result.error.message).toBe('Team limit reached');
|
||||
});
|
||||
|
||||
it('should fail when adding duplicate team', () => { /* ... */ });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Setting Up Test Fixtures
|
||||
|
||||
**Factory Functions:**
|
||||
```typescript
|
||||
// tests/support/factories.ts
|
||||
export function createTestLeague(overrides?: Partial<LeagueProps>): League {
|
||||
return new League('Test League', {
|
||||
maxTeams: 10,
|
||||
scoringSystem: 'F1',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
export function createTestTeam(name: string): Team {
|
||||
return new Team(name, { drivers: ['Driver 1', 'Driver 2'] });
|
||||
}
|
||||
```
|
||||
|
||||
### Mocking Ports in Use Case Tests
|
||||
|
||||
```typescript
|
||||
// tests/unit/application/CreateLeagueUseCase.test.ts
|
||||
describe('CreateLeagueUseCase', () => {
|
||||
let mockRepo: jest.Mocked<ILeagueRepository>;
|
||||
let useCase: CreateLeagueUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepo = {
|
||||
save: jest.fn().mockResolvedValue(undefined),
|
||||
findById: jest.fn().mockResolvedValue(null),
|
||||
findByName: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
useCase = new CreateLeagueUseCase(mockRepo);
|
||||
});
|
||||
|
||||
it('should create a league when name is unique', async () => {
|
||||
const result = await useCase.execute({ name: 'New League' });
|
||||
|
||||
expect(result.isSuccess()).toBe(true);
|
||||
expect(mockRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'New League' })
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Database Cleanup Strategies
|
||||
|
||||
**Integration Tests:**
|
||||
```typescript
|
||||
// tests/integration/setup.ts
|
||||
import { sql } from './database';
|
||||
|
||||
export async function cleanDatabase() {
|
||||
await sql`TRUNCATE TABLE event_results CASCADE`;
|
||||
await sql`TRUNCATE TABLE events CASCADE`;
|
||||
await sql`TRUNCATE TABLE teams CASCADE`;
|
||||
await sql`TRUNCATE TABLE leagues CASCADE`;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
```
|
||||
|
||||
**E2E Tests:**
|
||||
```typescript
|
||||
// tests/e2e/support/hooks.ts
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
export const test = base.extend({
|
||||
page: async ({ page }, use) => {
|
||||
// Clean database before each test
|
||||
await fetch('http://localhost:3000/test/cleanup', { method: 'POST' });
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Playwright Page Object Pattern
|
||||
|
||||
```typescript
|
||||
// tests/e2e/pages/LeaguePage.ts
|
||||
export class LeaguePage {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async navigateToCreateLeague() {
|
||||
await this.page.goto('/leagues/create');
|
||||
}
|
||||
|
||||
async fillLeagueForm(data: { name: string; maxTeams: number }) {
|
||||
await this.page.fill('[name="name"]', data.name);
|
||||
await this.page.fill('[name="maxTeams"]', data.maxTeams.toString());
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
await this.page.click('button[type="submit"]');
|
||||
}
|
||||
|
||||
async getSuccessMessage() {
|
||||
return this.page.textContent('.success-message');
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in test
|
||||
test('should create league', async ({ page }) => {
|
||||
const leaguePage = new LeaguePage(page);
|
||||
await leaguePage.navigateToCreateLeague();
|
||||
await leaguePage.fillLeagueForm({ name: 'Test', maxTeams: 10 });
|
||||
await leaguePage.submitForm();
|
||||
|
||||
expect(await leaguePage.getSuccessMessage()).toBe('League created');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
- **[`ARCHITECTURE.md`](./ARCHITECTURE.md)** — Layer boundaries, port definitions, and dependency rules that guide test structure
|
||||
- **[`TECH.md`](./TECH.md)** — Detailed tooling specifications (Vitest, Playwright, Testcontainers configuration)
|
||||
- **[`package.json`](../package.json)** — Test scripts and commands (`test:unit`, `test:integration`, `test:e2e`, `test:coverage`)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
GridPilot's testing strategy ensures:
|
||||
- **Business logic is correct** (unit tests for domain/application layers)
|
||||
- **Infrastructure works reliably** (integration tests for repositories/adapters)
|
||||
- **User workflows function end-to-end** (E2E tests for full stack)
|
||||
|
||||
By following BDD principles and maintaining clear test organization, the team can confidently evolve GridPilot while preserving correctness and stability.
|
||||
Reference in New Issue
Block a user