feat: add Playwright E2E and API regression test suite
Production-ready test infrastructure with Page Object Model pattern, reusable fixtures for auth/DB/test-data, and example tests covering login flow, dashboard, accounts CRUD API, and visual regression. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
349
TESTING_CONVENTIONS.md
Normal file
349
TESTING_CONVENTIONS.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Testing Conventions — HOA LedgerIQ E2E & API Tests
|
||||
|
||||
This document is the single source of truth for writing, organizing, and running Playwright-based E2E and API regression tests in this project.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
| Component | Technology | Port |
|
||||
|-----------|-----------|------|
|
||||
| Reverse proxy | nginx | :80 |
|
||||
| Backend API | NestJS 10 | :3000 (internal) |
|
||||
| Frontend | React 18 + Vite | :5173 (internal) |
|
||||
| Database | PostgreSQL 15 | :5432 |
|
||||
| Cache | Redis 7 | :6379 |
|
||||
| Test runner | Playwright | host |
|
||||
|
||||
Tests run on the **host machine** against the app running in **Docker Compose**. The `BASE_URL` defaults to `http://localhost` (nginx).
|
||||
|
||||
---
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── .auth/ # Stored auth state (gitignored)
|
||||
│ └── user.json # Browser state from auth.setup.ts
|
||||
├── fixtures/
|
||||
│ ├── auth.fixture.ts # API login helpers, token management
|
||||
│ ├── base.fixture.ts # Extended test object with typed fixtures
|
||||
│ ├── db.fixture.ts # Postgres seed/cleanup via pg driver
|
||||
│ └── test-data.ts # Shared constants (users, sample data)
|
||||
├── page-objects/
|
||||
│ ├── index.ts # Re-exports all page objects
|
||||
│ ├── BasePage.ts # Abstract base with shared helpers
|
||||
│ ├── LoginPage.ts # /login page
|
||||
│ ├── DashboardPage.ts # /dashboard page
|
||||
│ └── AccountsPage.ts # /accounts page
|
||||
├── e2e/ # Browser-based end-to-end tests
|
||||
│ ├── auth.spec.ts # Login/logout UI flows
|
||||
│ ├── dashboard.spec.ts # Dashboard load + navigation
|
||||
│ └── visual.spec.ts # Screenshot regression tests
|
||||
├── api/ # API-only tests (no browser)
|
||||
│ ├── auth.api.spec.ts # /api/auth/* endpoints
|
||||
│ └── accounts.api.spec.ts # /api/accounts/* CRUD
|
||||
└── auth.setup.ts # One-time auth setup project
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| What | Convention | Example |
|
||||
|------|-----------|---------|
|
||||
| E2E test files | `tests/e2e/<feature>.spec.ts` | `auth.spec.ts` |
|
||||
| API test files | `tests/api/<resource>.api.spec.ts` | `accounts.api.spec.ts` |
|
||||
| Page objects | `tests/page-objects/<PageName>.ts` | `LoginPage.ts` |
|
||||
| Fixtures | `tests/fixtures/<purpose>.fixture.ts` | `db.fixture.ts` |
|
||||
| Test data | `tests/fixtures/test-data.ts` | single file |
|
||||
| Snapshot baselines | auto-generated in `*-snapshots/` dirs | `login-page.png` |
|
||||
|
||||
### Test descriptions
|
||||
|
||||
Use `test.describe('Feature or Endpoint')` and `test('should <behavior>')`:
|
||||
|
||||
```ts
|
||||
test.describe('POST /api/auth/login', () => {
|
||||
test('should return access token for valid credentials', async ({ request }) => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Write New Tests
|
||||
|
||||
### 1. E2E (browser) test
|
||||
|
||||
```ts
|
||||
// tests/e2e/invoices.spec.ts
|
||||
import { test, expect } from '../fixtures/base.fixture';
|
||||
import { InvoicesPage } from '../page-objects';
|
||||
|
||||
test.describe('Invoices', () => {
|
||||
let invoicesPage: InvoicesPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
invoicesPage = new InvoicesPage(page);
|
||||
await invoicesPage.goto();
|
||||
});
|
||||
|
||||
test('should display invoice list', async () => {
|
||||
await invoicesPage.assertOnPage();
|
||||
// ... assertions
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. API test
|
||||
|
||||
```ts
|
||||
// tests/api/payments.api.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { apiLogin, apiSwitchOrg, authHeaders } from '../fixtures/auth.fixture';
|
||||
import { TEST_USERS } from '../fixtures/test-data';
|
||||
|
||||
const API_BASE = process.env.API_BASE_URL || 'http://localhost/api';
|
||||
let accessToken: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const tokens = await apiLogin(request, TEST_USERS.treasurer);
|
||||
if (tokens.organizations?.length > 0) {
|
||||
const switched = await apiSwitchOrg(request, tokens.accessToken, (tokens.organizations[0] as any).id);
|
||||
accessToken = switched.accessToken;
|
||||
} else {
|
||||
accessToken = tokens.accessToken;
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('GET /api/payments', () => {
|
||||
test('should return payments list', async ({ request }) => {
|
||||
const response = await request.get(`${API_BASE}/payments`, {
|
||||
headers: authHeaders(accessToken),
|
||||
});
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Visual regression test
|
||||
|
||||
```ts
|
||||
test('invoices page should match baseline', async ({ page }) => {
|
||||
await page.goto('/invoices');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(500); // Let animations settle
|
||||
|
||||
await expect(page).toHaveScreenshot('invoices-page.png', {
|
||||
fullPage: true,
|
||||
mask: [page.locator('time')], // Mask dynamic dates
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Update baselines: `npx playwright test --update-snapshots`
|
||||
|
||||
---
|
||||
|
||||
## How to Add New Page Objects
|
||||
|
||||
1. Create `tests/page-objects/MyPage.ts`:
|
||||
|
||||
```ts
|
||||
import { type Page, expect } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class MyPage extends BasePage {
|
||||
readonly path = '/my-path';
|
||||
|
||||
// Locators — prefer role/label selectors over CSS
|
||||
get heading() {
|
||||
return this.page.getByRole('heading', { name: /my page/i });
|
||||
}
|
||||
|
||||
get createButton() {
|
||||
return this.page.getByRole('button', { name: /create/i });
|
||||
}
|
||||
|
||||
// Actions
|
||||
override async waitForReady(): Promise<void> {
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await expect(this.heading).toBeVisible();
|
||||
}
|
||||
|
||||
async createItem(name: string): Promise<void> {
|
||||
await this.createButton.click();
|
||||
await this.page.getByLabel(/name/i).fill(name);
|
||||
await this.page.getByRole('button', { name: /save/i }).click();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Export from `tests/page-objects/index.ts`:
|
||||
|
||||
```ts
|
||||
export { MyPage } from './MyPage';
|
||||
```
|
||||
|
||||
### Page object rules
|
||||
|
||||
- Extend `BasePage` and set `readonly path`
|
||||
- Override `waitForReady()` for page-specific loading
|
||||
- Use **role/label locators** (not CSS selectors): `getByRole()`, `getByLabel()`, `getByText()`
|
||||
- Expose **locators as getters** and **actions as methods**
|
||||
- Keep assertions in test files, not page objects (except `assertOnPage()`)
|
||||
|
||||
---
|
||||
|
||||
## Authentication in Tests
|
||||
|
||||
### Pre-authenticated tests (default)
|
||||
|
||||
Most tests use stored auth state from `auth.setup.ts`. This runs once via the `auth-setup` Playwright project and saves browser state to `tests/.auth/user.json`.
|
||||
|
||||
Tests automatically get this state via `storageState` in `playwright.config.ts`.
|
||||
|
||||
### Unauthenticated tests
|
||||
|
||||
For testing the login flow itself, opt out:
|
||||
|
||||
```ts
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
```
|
||||
|
||||
### API tests
|
||||
|
||||
Use the `apiLogin()` and `authHeaders()` helpers:
|
||||
|
||||
```ts
|
||||
import { apiLogin, authHeaders } from '../fixtures/auth.fixture';
|
||||
|
||||
const tokens = await apiLogin(request, TEST_USERS.treasurer);
|
||||
const response = await request.get(url, {
|
||||
headers: authHeaders(tokens.accessToken),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Seeding & Cleanup
|
||||
|
||||
### When to use direct DB access
|
||||
|
||||
- Verifying backend wrote correct data
|
||||
- Seeding complex state that's hard to create via API
|
||||
- Cleanup after tests
|
||||
|
||||
### How
|
||||
|
||||
```ts
|
||||
import { test } from '../fixtures/base.fixture';
|
||||
|
||||
test('should verify data', async ({ db }) => {
|
||||
const result = await db.query('SELECT * FROM schema.table WHERE ...');
|
||||
expect(result.rows.length).toBeGreaterThan(0);
|
||||
});
|
||||
```
|
||||
|
||||
### Cleanup convention
|
||||
|
||||
- Prefix all test-created data with `E2E_` (use `TEST_PREFIX` from test-data.ts)
|
||||
- The `db.cleanup()` method deletes rows matching this prefix
|
||||
- Call `db.cleanup()` in `test.afterAll` for write-path tests
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Docker Compose services running: `docker-compose up -d`
|
||||
2. Test user seeded in the database (use the backend seed script or create manually)
|
||||
3. Environment configured: `cp .env.test.example .env.test` and fill in values
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Install Playwright (first time)
|
||||
npx playwright install --with-deps
|
||||
|
||||
# Run all tests
|
||||
npx playwright test
|
||||
|
||||
# Run only E2E tests
|
||||
npx playwright test tests/e2e/
|
||||
|
||||
# Run only API tests
|
||||
npx playwright test --project=api
|
||||
|
||||
# Run in specific browser
|
||||
npx playwright test --project=chromium
|
||||
|
||||
# Run in headed mode (see the browser)
|
||||
npx playwright test --headed
|
||||
|
||||
# Run a single test file
|
||||
npx playwright test tests/e2e/auth.spec.ts
|
||||
|
||||
# Debug mode (step through tests)
|
||||
npx playwright test --debug
|
||||
|
||||
# Update visual regression baselines
|
||||
npx playwright test tests/e2e/visual.spec.ts --update-snapshots
|
||||
|
||||
# View HTML report
|
||||
npx playwright show-report
|
||||
|
||||
# Run against production
|
||||
BASE_URL=https://your-prod-domain.com npx playwright test --project=api
|
||||
```
|
||||
|
||||
### npm scripts (from project root)
|
||||
|
||||
```bash
|
||||
npm run test:e2e # All Playwright tests
|
||||
npm run test:e2e:chromium # Chromium only
|
||||
npm run test:e2e:api # API tests only
|
||||
npm run test:e2e:headed # Headed mode
|
||||
npm run test:e2e:debug # Debug mode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `BASE_URL` | `http://localhost` | App URL (nginx) |
|
||||
| `API_BASE_URL` | `http://localhost/api` | Backend API base |
|
||||
| `TEST_DB_URL` | `postgresql://hoafinance:change_me@localhost:5432/hoafinance` | Direct Postgres for seeding |
|
||||
| `TEST_USER_EMAIL` | `e2e-treasurer@test.hoaledgeriq.com` | Test user email |
|
||||
| `TEST_USER_PASSWORD` | `TestPass123!` | Test user password |
|
||||
| `CI` | — | Set by CI providers; enables retries, single worker |
|
||||
|
||||
---
|
||||
|
||||
## Style Rules
|
||||
|
||||
1. **Import `test` from `../fixtures/base.fixture`** for tests needing DB or auth fixtures. Import from `@playwright/test` for basic tests.
|
||||
2. **One `test.describe` per feature or endpoint** per file.
|
||||
3. **No `page.waitForTimeout()` except in visual tests** — use `waitForLoadState`, `waitForURL`, or `waitForResponse` instead.
|
||||
4. **No hardcoded URLs** — use `BASE_URL`, `API_BASE`, or page object paths.
|
||||
5. **No test interdependencies** — each test should work in isolation (use `test.beforeEach` for setup).
|
||||
6. **Clean up after write tests** — use `TEST_PREFIX` and `db.cleanup()`.
|
||||
7. **API tests go in `tests/api/`**, E2E tests in `tests/e2e/`** — don't mix.
|
||||
8. **Locators**: prefer `getByRole` > `getByLabel` > `getByText` > `getByTestId` > CSS selectors.
|
||||
|
||||
---
|
||||
|
||||
## Adding Tests for a New Feature (Quick Checklist)
|
||||
|
||||
- [ ] Create page object in `tests/page-objects/` if it's a new page
|
||||
- [ ] Export it from `tests/page-objects/index.ts`
|
||||
- [ ] Create `tests/e2e/<feature>.spec.ts` for UI flows
|
||||
- [ ] Create `tests/api/<resource>.api.spec.ts` for API endpoints
|
||||
- [ ] Add sample data constants to `tests/fixtures/test-data.ts` if needed
|
||||
- [ ] Run `npx playwright test tests/e2e/<feature>.spec.ts` to verify
|
||||
- [ ] Update visual baselines if the feature changes existing pages
|
||||
Reference in New Issue
Block a user