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>
350 lines
10 KiB
Markdown
350 lines
10 KiB
Markdown
# 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
|