be932c1930
- sprint12-analysis.md (full page audit) - sprint12-plan.md (button fix plan) - sprint12-testplan.md (button fix test plan) - sprint12-phase2-integration-tests.md (v3, expert-approved) - sprint12-phase2-panel-review.md (3 review cycles, 95% confidence) - sprint12-code-review.md (approved with comments, blockers fixed)
1000 lines
46 KiB
Markdown
1000 lines
46 KiB
Markdown
# Sprint 12 Phase 2: Real Integration Tests with Seed DB
|
||
|
||
**Date:** 2026-06-18
|
||
**Author:** Patrick Plate / Lumen (Planner)
|
||
**Status:** v3 — final revision per panel re-review
|
||
**Goal:** Replace demo/mock-mode E2E tests with full-stack integration tests backed by a real PostgreSQL database
|
||
|
||
---
|
||
|
||
## 1. Seed Data Strategy
|
||
|
||
### 1.1 Current State
|
||
|
||
The existing `scripts/seed/init.sql` already provides a baseline:
|
||
|
||
| Entity | Count | Notes |
|
||
|--------|-------|-------|
|
||
| Club | 1 | "Grüner Daumen e.V." |
|
||
| Admin User | 1 | admin@test.de / test123 |
|
||
| Members | 6 | Various statuses, 1 under-21 (Jonas Weber, DOB 2007-03-15), 1 near-quota (23g/25g used this month) |
|
||
| Strains | 3 | Northern Lights (18.5% THC), Amnesia Haze (22% THC), CBD Critical Mass (0.5% THC / 12% CBD) |
|
||
| Batches | 3 | All AVAILABLE |
|
||
| Distributions | 3 | From December 2024 |
|
||
| Monthly Quotas | 3 | December 2024 |
|
||
| Stock Movements | 4 | HARVEST_IN + DISTRIBUTION |
|
||
| Fee Schedules | 2 | Regulär + Ermäßigt |
|
||
| Fee Assignments | 4 | Members → fee schedules |
|
||
| Payments | 3 | PAID, various methods |
|
||
| Board Positions | 2 | 1. Vorsitzender + Kassenwart |
|
||
| Board Members | 2 | Max + Lisa elected |
|
||
|
||
### 1.2 Missing Seed Data (to add)
|
||
|
||
| Entity | Table | Count | Purpose |
|
||
|--------|-------|-------|---------|
|
||
| Staff User | `users` | 1 | staff@test.de / test123 (ROLE_STAFF) — for role-based tests |
|
||
| Member User | `users` | 1 | max@test.de / test123 (ROLE_MEMBER) — portal tests |
|
||
| Near-quota Member | `members` + `monthly_quotas` | 1 | "Thomas Müller" — 23g of 25g used this month (edge case D-3) |
|
||
| Documents | `documents` | 4 | One per category (SATZUNG, PROTOKOLL, VERTRAG, SONSTIGES) |
|
||
| Events | `club_events` | 3 | 1 past, 1 today, 1 future |
|
||
| Event RSVPs | `event_rsvps` | 3 | Various statuses |
|
||
| Info Board Posts | `info_board_posts` | 3 | 1 pinned, 2 normal |
|
||
| Forum Topics | `forum_topics` | 2 | 1 pinned, 1 regular |
|
||
| Forum Replies | `forum_replies` | 3 | 2 on topic 1, 1 on topic 2 |
|
||
| Grow Entries | `grow_calendar` (V9) | 3 | SEEDLING, VEGETATIVE, FLOWERING stages |
|
||
| Compliance Deadlines | `compliance_deadlines` (V28) | 3 | PENDING, OVERDUE, COMPLETED |
|
||
| Destruction Records | `destruction_records` (V23) | 1 | Compliance audit trail (v3: `recorded_by` = admin UUID `b1000000-...001`, not member UUID) |
|
||
|
||
### 1.3 Seed File Architecture
|
||
|
||
**Decision (v2): Flyway-only seeding — NO Docker `docker-entrypoint-initdb.d` mount.**
|
||
|
||
The seed data is loaded exclusively via a Flyway repeatable migration (`R__seed_test_data.sql`). This eliminates the timing contradiction where `docker-entrypoint-initdb.d` runs BEFORE Flyway creates the schema.
|
||
|
||
```
|
||
cannamanage-api/src/main/resources/db/
|
||
├── migration/ ← versioned migrations (V1..V35+)
|
||
│ ├── V1__initial_schema.sql
|
||
│ ├── ...
|
||
│ └── V35__xxx.sql
|
||
└── testdata/ ← test-only seed (Flyway repeatable)
|
||
└── R__seed_test_data.sql ← single file, includes all seed data
|
||
```
|
||
|
||
**Activation:** Only when `test` Spring profile is active:
|
||
|
||
```properties
|
||
# application-test.properties
|
||
spring.flyway.locations=classpath:db/migration,classpath:db/testdata
|
||
```
|
||
|
||
**Production/default profiles** only load `classpath:db/migration` — seed data is never deployed to production.
|
||
|
||
For local development reference, modular fragments remain under `scripts/seed/fragments/` for documentation and manual use:
|
||
|
||
```
|
||
scripts/seed/fragments/ ← reference fragments (not loaded by Flyway)
|
||
├── 00-club.sql
|
||
├── 01-users.sql
|
||
├── 02-members.sql
|
||
├── 03-strains-batches.sql
|
||
├── 04-distributions.sql
|
||
├── 05-finance.sql
|
||
├── 06-board.sql
|
||
├── 07-documents.sql
|
||
├── 08-events.sql
|
||
├── 09-forum.sql
|
||
├── 10-info-board.sql
|
||
├── 11-grow.sql
|
||
└── 12-compliance.sql
|
||
```
|
||
|
||
These fragments are concatenated into `R__seed_test_data.sql` during development. The single-file approach ensures Flyway checksum tracking works correctly.
|
||
|
||
### 1.4 Seed Data Design Principles
|
||
|
||
1. **Deterministic UUIDs** — All IDs follow the pattern `{prefix}000000-0000-0000-0000-00000000000{N}` for predictability in assertions
|
||
2. **ON CONFLICT DO NOTHING** — Idempotent inserts, safe to re-run (Flyway repeatable migration re-executes on checksum change)
|
||
3. **Realistic dates** — Use relative dates where possible (`NOW() - INTERVAL '7 days'`) for time-sensitive tests
|
||
4. **KCanG edge cases built-in** — Under-21 member (THC/quota limits), near-quota member (23g/25g), high-THC strain (22%), recalled batch, overdue compliance deadline
|
||
5. **All FKs satisfied** — Every row references valid parents (tenant_id, club_id, user_id)
|
||
|
||
### 1.5 Test Accounts
|
||
|
||
| Email | Password | Role | Purpose |
|
||
|-------|----------|------|---------|
|
||
| admin@test.de | test123 | ROLE_ADMIN | Full admin dashboard access |
|
||
| staff@test.de | test123 | ROLE_STAFF | Staff-level access (limited) |
|
||
| max@test.de | test123 | ROLE_MEMBER | Member portal access |
|
||
|
||
---
|
||
|
||
## 2. Test Architecture
|
||
|
||
### 2.1 DB Reset Strategy
|
||
|
||
**Decision (v2): Per-test reset via backend API endpoint + `beforeEach` hook.**
|
||
|
||
The backend exposes a test-only endpoint (gated by `test` Spring profile):
|
||
|
||
```
|
||
POST /api/v1/test/reset-db
|
||
Authorization: Bearer <admin-token>
|
||
Profile: test (only available when SPRING_PROFILES_ACTIVE includes "test")
|
||
```
|
||
|
||
This endpoint will:
|
||
1. `TRUNCATE ... CASCADE` all application tables (preserving Flyway schema_history)
|
||
2. Re-execute `R__seed_test_data.sql` content via JdbcTemplate
|
||
3. Return 200 OK when ready
|
||
|
||
**Why per-test (not per-suite)?**
|
||
- Panel finding R-1: If test 3 (create) fails mid-way, test 4 (delete) will also fail due to dirty state
|
||
- TRUNCATE + INSERT is <500ms — acceptable per-test overhead for 60+ tests
|
||
- Each test starts from identical seed state — no ordering dependencies
|
||
|
||
**Integration in Playwright:**
|
||
|
||
```typescript
|
||
// e2e/integration/helpers/db-reset.ts
|
||
import { ApiClient } from './api-client';
|
||
|
||
export async function resetDatabase(apiClient: ApiClient) {
|
||
const response = await apiClient.resetDb();
|
||
if (response.status !== 200) {
|
||
throw new Error(`DB reset failed: ${response.status}`);
|
||
}
|
||
}
|
||
```
|
||
|
||
Each spec file uses `beforeEach`:
|
||
|
||
```typescript
|
||
test.describe.serial('Members', () => {
|
||
test.beforeEach(async () => {
|
||
await apiClient.resetDb();
|
||
});
|
||
|
||
test('Members table shows all 6 seed members', async ({ page }) => { ... });
|
||
test('Create new member', async ({ page }) => { ... });
|
||
});
|
||
```
|
||
|
||
**Why not pg_restore or fresh container?**
|
||
- Container restart is too slow (15-30s for Spring Boot + Flyway)
|
||
- TRUNCATE + INSERT is <500ms
|
||
- Keeps the Docker orchestration simple
|
||
|
||
### 2.2 Selector Strategy — `data-testid` Attributes
|
||
|
||
**Decision (v2): Commit to `data-testid` attributes for all testable UI elements.**
|
||
|
||
This is NOT an open question — it is a requirement for Phase 2 implementation. Every interactive element and data-display element that integration tests assert on MUST have a `data-testid` attribute.
|
||
|
||
**Naming convention:**
|
||
|
||
```
|
||
data-testid="<page>-<component>-<identifier>"
|
||
```
|
||
|
||
Examples:
|
||
- `data-testid="members-table"` — the members list table
|
||
- `data-testid="members-row-{id}"` — individual member row
|
||
- `data-testid="members-create-btn"` — create button
|
||
- `data-testid="members-form-email"` — email input in create/edit form
|
||
- `data-testid="distributions-quota-display"` — quota usage display
|
||
- `data-testid="dashboard-member-count"` — member count card
|
||
- `data-testid="nav-item-{slug}"` — navigation items
|
||
|
||
**Shared selectors file:**
|
||
|
||
```typescript
|
||
// e2e/integration/helpers/selectors.ts
|
||
export const SELECTORS = {
|
||
members: {
|
||
table: '[data-testid="members-table"]',
|
||
row: (id: string) => `[data-testid="members-row-${id}"]`,
|
||
createBtn: '[data-testid="members-create-btn"]',
|
||
formEmail: '[data-testid="members-form-email"]',
|
||
formName: '[data-testid="members-form-name"]',
|
||
formSubmit: '[data-testid="members-form-submit"]',
|
||
},
|
||
distributions: {
|
||
table: '[data-testid="distributions-table"]',
|
||
quotaDisplay: '[data-testid="distributions-quota-display"]',
|
||
newBtn: '[data-testid="distributions-create-btn"]',
|
||
},
|
||
dashboard: {
|
||
memberCount: '[data-testid="dashboard-member-count"]',
|
||
stockSummary: '[data-testid="dashboard-stock-summary"]',
|
||
},
|
||
// ... more selectors per page
|
||
} as const;
|
||
```
|
||
|
||
**Implementation impact:** Phase 2 implementation MUST add `data-testid` attributes to frontend components being tested. This is tracked as a sub-task of Phase 2C.
|
||
|
||
### 2.8 Seed Constants — Single Source of Truth for Test Assertions (v3: R-4)
|
||
|
||
**New file: `cannamanage-frontend/e2e/seed-constants.ts`**
|
||
|
||
All test assertions referencing seed data MUST import expected values from this file. When the seed SQL changes, update this one file — not 13 spec files.
|
||
|
||
```typescript
|
||
// cannamanage-frontend/e2e/seed-constants.ts
|
||
// Single source of truth for values derived from R__seed_test_data.sql
|
||
|
||
// ─── Deterministic UUIDs ───────────────────────────────────────────
|
||
export const CLUB_ID = 'a1000000-0000-0000-0000-000000000001';
|
||
|
||
export const ADMIN_USER_ID = 'b1000000-0000-0000-0000-000000000001';
|
||
export const STAFF_USER_ID = 'b1000000-0000-0000-0000-000000000002';
|
||
export const MEMBER_USER_ID = 'b1000000-0000-0000-0000-000000000003';
|
||
|
||
export const MEMBERS = {
|
||
MAX_MUSTERMANN: { id: 'c1000000-0000-0000-0000-000000000001', name: 'Max Mustermann', email: 'max@test.de' },
|
||
LISA_MEYER: { id: 'c1000000-0000-0000-0000-000000000002', name: 'Lisa Meyer' },
|
||
JONAS_WEBER: { id: 'c1000000-0000-0000-0000-000000000003', name: 'Jonas Weber', isUnder21: true },
|
||
THOMAS_MUELLER: { id: 'c1000000-0000-0000-0000-000000000004', name: 'Thomas Müller', quotaUsedG: 23 },
|
||
SARAH_SCHMIDT: { id: 'c1000000-0000-0000-0000-000000000005', name: 'Sarah Schmidt' },
|
||
ANNA_BRAUN: { id: 'c1000000-0000-0000-0000-000000000006', name: 'Anna Braun' },
|
||
} as const;
|
||
|
||
export const MEMBER_COUNT = 6;
|
||
|
||
// ─── Strains & Batches ────────────────────────────────────────────
|
||
export const STRAINS = {
|
||
NORTHERN_LIGHTS: { id: 'd1000000-0000-0000-0000-000000000001', name: 'Northern Lights', thcPct: 18.5 },
|
||
AMNESIA_HAZE: { id: 'd1000000-0000-0000-0000-000000000002', name: 'Amnesia Haze', thcPct: 22.0 },
|
||
CBD_CRITICAL_MASS: { id: 'd1000000-0000-0000-0000-000000000003', name: 'CBD Critical Mass', thcPct: 0.5, cbdPct: 12.0 },
|
||
} as const;
|
||
|
||
export const BATCH_COUNT = 3;
|
||
|
||
// ─── Distributions ────────────────────────────────────────────────
|
||
export const DISTRIBUTION_COUNT = 3;
|
||
export const DISTRIBUTION_QUANTITIES_G = [5, 3, 2];
|
||
|
||
// ─── Finance ──────────────────────────────────────────────────────
|
||
export const PAYMENT_COUNT = 3;
|
||
export const PAYMENT_AMOUNT_EUR = 30;
|
||
export const FEE_REGULAR_EUR = 30;
|
||
export const FEE_REDUCED_EUR = 15;
|
||
|
||
// ─── KCanG Quota Limits ───────────────────────────────────────────
|
||
export const KCANG = {
|
||
ADULT_DAILY_LIMIT_G: 25,
|
||
ADULT_MONTHLY_LIMIT_G: 50,
|
||
UNDER21_MONTHLY_LIMIT_G: 30,
|
||
UNDER21_MAX_THC_PCT: 10,
|
||
} as const;
|
||
|
||
// ─── Board ────────────────────────────────────────────────────────
|
||
export const BOARD_POSITIONS = ['1. Vorsitzender', 'Kassenwart'];
|
||
|
||
// ─── Documents ────────────────────────────────────────────────────
|
||
export const DOCUMENT_COUNT = 4;
|
||
export const DOCUMENT_CATEGORIES = ['SATZUNG', 'PROTOKOLL', 'VERTRAG', 'SONSTIGES'];
|
||
|
||
// ─── Events ───────────────────────────────────────────────────────
|
||
export const EVENT_COUNT = 3;
|
||
|
||
// ─── Forum ────────────────────────────────────────────────────────
|
||
export const FORUM_TOPIC_COUNT = 2;
|
||
export const FORUM_REPLY_COUNT = 3;
|
||
|
||
// ─── Grow ─────────────────────────────────────────────────────────
|
||
export const GROW_ENTRY_COUNT = 3;
|
||
export const GROW_STAGES = ['SEEDLING', 'VEGETATIVE', 'FLOWERING'];
|
||
|
||
// ─── Compliance ───────────────────────────────────────────────────
|
||
export const COMPLIANCE_DEADLINE_COUNT = 3;
|
||
export const COMPLIANCE_STATUSES = ['PENDING', 'OVERDUE', 'COMPLETED'];
|
||
```
|
||
|
||
**Usage in tests:**
|
||
|
||
```typescript
|
||
// e2e/integration/02-members.spec.ts
|
||
import { MEMBERS, MEMBER_COUNT } from '../seed-constants';
|
||
|
||
test('Members table shows all seed members', async ({ page }) => {
|
||
await page.goto('/members');
|
||
const rows = page.locator(SELECTORS.members.table + ' tbody tr');
|
||
await expect(rows).toHaveCount(MEMBER_COUNT);
|
||
await expect(page.locator(SELECTORS.members.row(MEMBERS.MAX_MUSTERMANN.id)))
|
||
.toContainText(MEMBERS.MAX_MUSTERMANN.name);
|
||
});
|
||
```
|
||
|
||
**Rule:** Never hardcode seed-derived values in spec files. Always import from `seed-constants.ts`. When `R__seed_test_data.sql` changes, update `seed-constants.ts` — all tests automatically adapt.
|
||
|
||
### 2.3 Playwright Config Changes
|
||
|
||
Add a new project `integration` to `playwright.config.ts`:
|
||
|
||
```typescript
|
||
{
|
||
name: "integration",
|
||
testMatch: /integration\/.+\.spec\.ts/,
|
||
dependencies: ["setup"],
|
||
timeout: 90_000,
|
||
expect: {
|
||
timeout: 15_000, // v2: extended for API-dependent assertions (panel R-3)
|
||
},
|
||
use: {
|
||
storageState: authFile,
|
||
browserName: "chromium",
|
||
navigationTimeout: 60_000,
|
||
},
|
||
}
|
||
```
|
||
|
||
**Timeout rationale (v2, panel R-3):**
|
||
- `timeout: 90_000` — overall test timeout, appropriate for real backend with Docker networking
|
||
- `navigationTimeout: 60_000` — page loads through Docker proxy
|
||
- `expect.timeout: 15_000` — assertions may wait for API responses; default 5s is too short for DB-backed assertions
|
||
- **First-test warmup:** The first test in a suite may be 5-10s slower due to connection pool warmup, JIT compilation, and first-request overhead. This is expected — `expect.timeout: 15_000` accommodates it.
|
||
|
||
### 2.4 Test File Organization
|
||
|
||
```
|
||
cannamanage-frontend/e2e/
|
||
├── global-setup.ts ← auth + health check wait (v2: A-5)
|
||
├── integration/ ← NEW: integration test specs
|
||
│ ├── helpers/
|
||
│ │ ├── api-client.ts ← direct API calls for setup/teardown/reset
|
||
│ │ ├── db-reset.ts ← DB reset helper (v2: R-1)
|
||
│ │ ├── selectors.ts ← shared data-testid selectors (v2: R-2)
|
||
│ │ └── assertions.ts ← reusable assertion helpers
|
||
│ ├── 01-dashboard.spec.ts
|
||
│ ├── 02-members.spec.ts
|
||
│ ├── 03-distributions.spec.ts
|
||
│ ├── 04-stock.spec.ts
|
||
│ ├── 05-documents.spec.ts
|
||
│ ├── 06-board.spec.ts
|
||
│ ├── 07-calendar.spec.ts
|
||
│ ├── 08-forum.spec.ts
|
||
│ ├── 09-info-board.spec.ts
|
||
│ ├── 10-finance.spec.ts
|
||
│ ├── 11-grow.spec.ts
|
||
│ ├── 12-compliance.spec.ts
|
||
│ └── 13-kcang-regulatory.spec.ts ← NEW (v2: D-1/D-2 KCanG edge cases)
|
||
├── system-test.spec.ts ← existing (keep as smoke test)
|
||
└── ... ← existing specs (keep)
|
||
```
|
||
|
||
### 2.5 Helper: API Client
|
||
|
||
A thin wrapper for direct backend API calls (for verification, setup, and DB reset):
|
||
|
||
```typescript
|
||
// e2e/integration/helpers/api-client.ts
|
||
export class ApiClient {
|
||
constructor(private baseUrl: string, private token: string) {}
|
||
|
||
async resetDb() { /* POST /api/v1/test/reset-db */ }
|
||
async getMembers() { /* GET /api/v1/members */ }
|
||
async getDistributions() { /* GET /api/v1/distributions */ }
|
||
async getBatches() { /* GET /api/v1/stock/batches */ }
|
||
async getMemberQuota(memberId: string) { /* GET /api/v1/members/{id}/quota */ }
|
||
// ... other endpoints for DB state verification
|
||
}
|
||
```
|
||
|
||
This allows tests to verify DB state after UI actions without relying solely on UI assertions.
|
||
|
||
### 2.6 Auth Flow
|
||
|
||
The existing `global-setup.ts` already handles admin login and saves `storageState`. Integration tests will:
|
||
1. Reuse the saved admin auth state (no per-test login overhead)
|
||
2. For member portal tests: create a separate auth state file (`member.json`)
|
||
|
||
### 2.7 Global Setup — Health Check Wait (v2: A-5)
|
||
|
||
Before any test runs, `global-setup.ts` waits for both backend and frontend to be healthy:
|
||
|
||
```typescript
|
||
// e2e/global-setup.ts
|
||
async function globalSetup() {
|
||
// Wait for backend health
|
||
await waitForUrl('http://backend:8080/actuator/health', { timeout: 120_000, interval: 3_000 });
|
||
// Wait for frontend
|
||
await waitForUrl('http://frontend:3000', { timeout: 60_000, interval: 2_000 });
|
||
// Perform initial DB reset to ensure clean state
|
||
const apiClient = new ApiClient('http://backend:8080', adminToken);
|
||
await apiClient.resetDb();
|
||
// Authenticate and save state
|
||
// ...
|
||
}
|
||
```
|
||
|
||
This prevents flaky first-test failures due to services not being ready.
|
||
|
||
---
|
||
|
||
## 3. Integration Test Specs
|
||
|
||
### 3.1 Dashboard (`01-dashboard.spec.ts`)
|
||
|
||
**Seed data needed:** All (dashboard aggregates from multiple tables)
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Dashboard shows member count | Read | `[data-testid="dashboard-member-count"]` contains "6" |
|
||
| 2 | Dashboard shows stock summary | Read | Batch quantities match seed |
|
||
| 3 | Dashboard shows recent distributions | Read | Latest distribution visible |
|
||
| 4 | Dashboard shows compliance status | Read | Status indicator present |
|
||
| 5 | All dashboard cards load without error | Read | No loading spinners after `expect.timeout` |
|
||
|
||
### 3.2 Members (`02-members.spec.ts`)
|
||
|
||
**Seed data needed:** 6 members with various statuses (incl. under-21 + near-quota)
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Members table shows all 6 seed members | Read | Table rows = 6, names match |
|
||
| 2 | Member search/filter works | Read | Filter by "Max" → 1 result |
|
||
| 3 | Create new member | CRUD | Fill form → submit → toast → table has 7 rows |
|
||
| 4 | Edit existing member | CRUD | Click edit → change name → save → verify new name |
|
||
| 5 | Member detail shows correct data | Read | Click row → detail matches seed (DOB, email, status) |
|
||
| 6 | Under-21 member shows quota indicator | Read | Jonas Weber row shows age-restriction indicator |
|
||
| 7 | Near-quota member shows warning | Read | Thomas Müller row shows "23g/25g" usage warning |
|
||
|
||
**DB verification after test 3:**
|
||
```typescript
|
||
const members = await apiClient.getMembers();
|
||
expect(members.length).toBe(7);
|
||
expect(members.find(m => m.email === 'new@test.de')).toBeTruthy();
|
||
```
|
||
|
||
### 3.3 Distributions (`03-distributions.spec.ts`)
|
||
|
||
**Seed data needed:** 3 distributions, 6 members, 3 batches
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Distributions table shows seed data | Read | 3 rows, quantities match (5g, 3g, 2g) |
|
||
| 2 | Record new distribution | CRUD | Select member + batch → enter grams → submit |
|
||
| 3 | New distribution updates quota display | CRUD | Member's used quota increases |
|
||
| 4 | Under-21 member cannot exceed 30g/month quota | CRUD | Attempt 31g → error toast |
|
||
| 5 | Distribution shows batch strain name | Read | "Northern Lights" visible in row |
|
||
| 6 | THC/CBD values display correctly | Read | Verify THC% / CBD% columns |
|
||
|
||
### 3.4 Stock (`04-stock.spec.ts`)
|
||
|
||
**Seed data needed:** 3 batches (all AVAILABLE), 3 strains
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Stock table shows 3 batches | Read | Rows = 3, batch codes match |
|
||
| 2 | Batch details show strain info | Read | "Northern Lights 18.5% THC" |
|
||
| 3 | Add new batch (receive stock) | CRUD | Fill form → submit → 4 rows |
|
||
| 4 | Stock movement logged | CRUD | After receive, movement audit visible |
|
||
| 5 | Recall batch | CRUD | Click recall → status changes to RECALLED |
|
||
| 6 | Recalled batch not available for distribution | Read | Not in distribution dropdown |
|
||
|
||
### 3.5 Documents (`05-documents.spec.ts`)
|
||
|
||
**Seed data needed:** 4 documents across categories
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Documents page shows 4 seed documents | Read | Table rows = 4, titles visible |
|
||
| 2 | Filter by category works | Read | Filter "SATZUNG" → 1 result |
|
||
| 3 | Upload new document | CRUD | Select file → fill title/category → upload → toast |
|
||
| 4 | New document appears in list | Read | After upload, table has 5 rows |
|
||
| 5 | Download document | CRUD | Click download → verify response (non-empty) |
|
||
| 6 | Delete document | CRUD | Click delete → confirm → table has 4 rows again |
|
||
| 7 | Category badges display correctly | Read | Color-coded badges match category |
|
||
|
||
**Note:** For upload testing, Playwright can use `page.setInputFiles()` with a small test PDF.
|
||
|
||
### 3.6 Board (`06-board.spec.ts`)
|
||
|
||
**Seed data needed:** 2 positions, 2 board members
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Board page shows 2 positions | Read | "1. Vorsitzender", "Kassenwart" visible |
|
||
| 2 | Positions show elected members | Read | "Max Mustermann", "Lisa Meyer" visible |
|
||
| 3 | Create new position | CRUD | Click "Position hinzufügen" → fill title → save |
|
||
| 4 | Elect member to new position | CRUD | Click "Mitglied zuweisen" → select → confirm |
|
||
| 5 | Remove board member | CRUD | Click remove → confirm dialog → position shows vacant |
|
||
| 6 | Term dates display correctly | Read | "15.01.2024 – 15.01.2026" visible |
|
||
|
||
### 3.7 Calendar / Events (`07-calendar.spec.ts`)
|
||
|
||
**Seed data needed:** 3 events (past, today, future)
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Calendar shows events | Read | At least 1 event dot/indicator visible |
|
||
| 2 | Event detail shows correct info | Read | Click event → title, time, location visible |
|
||
| 3 | Create new event | CRUD | Fill form (title, date, type) → save |
|
||
| 4 | RSVP to event | CRUD | Click RSVP → status changes to "Zugesagt" |
|
||
| 5 | Cancel event | CRUD | Click cancel → event marked as cancelled |
|
||
|
||
### 3.8 Forum (`08-forum.spec.ts`)
|
||
|
||
**Seed data needed:** 2 topics, 3 replies
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Forum shows 2 topics | Read | Topic titles visible, reply counts shown |
|
||
| 2 | Pinned topic appears first | Read | First topic has pin indicator |
|
||
| 3 | Open topic shows replies | Read | Click topic → 2 replies visible |
|
||
| 4 | Create new topic | CRUD | Fill title + content → submit → 3 topics |
|
||
| 5 | Reply to topic | CRUD | Open topic → type reply → submit → reply count +1 |
|
||
| 6 | Topic search/filter works | Read | Search term → filtered results |
|
||
|
||
### 3.9 Info Board (`09-info-board.spec.ts`)
|
||
|
||
**Seed data needed:** 3 posts (1 pinned, 2 normal)
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Info board shows 3 posts | Read | Post cards/rows = 3 |
|
||
| 2 | Pinned post appears first | Read | First post has pin indicator |
|
||
| 3 | Create new announcement | CRUD | Fill title + content + category → publish |
|
||
| 4 | Archive post | CRUD | Click archive → post disappears from main view |
|
||
| 5 | Category filter works | Read | Select category → filtered results |
|
||
|
||
### 3.10 Finance (`10-finance.spec.ts`)
|
||
|
||
**Seed data needed:** 3 payments, 2 fee schedules
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Finance overview shows payment summary | Read | Total amounts visible |
|
||
| 2 | Payments table shows 3 entries | Read | Rows = 3, amounts match (30€, 30€, 30€) |
|
||
| 3 | Record new payment | CRUD | Select member → amount → method → save |
|
||
| 4 | Payment status badge shows correctly | Read | "PAID" badges on all seed entries |
|
||
| 5 | Fee schedule overview shows tiers | Read | "Regulär 30€" and "Ermäßigt 15€" visible |
|
||
|
||
### 3.11 Grow (`11-grow.spec.ts`)
|
||
|
||
**Seed data needed:** 3 grow entries at different stages
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Grow page shows entries | Read | 3 grow cards/rows visible |
|
||
| 2 | Stage indicators display correctly | Read | SEEDLING / VEGETATIVE / FLOWERING labels |
|
||
| 3 | Create new grow entry | CRUD | Fill strain + planted date + stage → save |
|
||
| 4 | Update grow stage | CRUD | Change stage SEEDLING → VEGETATIVE → verify |
|
||
| 5 | Grow timeline/history visible | Read | Stage transitions logged |
|
||
|
||
### 3.12 Compliance (`12-compliance.spec.ts`)
|
||
|
||
**Seed data needed:** 3 compliance deadlines (PENDING, OVERDUE, COMPLETED)
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Compliance dashboard shows status | Read | Overall compliance indicator |
|
||
| 2 | Deadlines list shows 3 entries | Read | Status badges match (PENDING, OVERDUE, COMPLETED) |
|
||
| 3 | Overdue items highlighted | Read | Red/warning indicator on overdue item |
|
||
| 4 | Mark deadline as completed | CRUD | Click complete → status changes |
|
||
| 5 | Reports section accessible | Read | Navigation to reports works |
|
||
|
||
### 3.13 KCanG Regulatory Edge Cases (`13-kcang-regulatory.spec.ts`) — NEW v2
|
||
|
||
**Seed data needed:** Under-21 member (Jonas Weber), near-quota member (Thomas Müller, 23g/25g), high-THC strain (Amnesia Haze 22%)
|
||
|
||
This spec specifically tests KCanG (Konsumcannabisgesetz) regulatory enforcement:
|
||
|
||
| # | Test Case | Type | Assertion |
|
||
|---|-----------|------|-----------|
|
||
| 1 | Daily 25g limit: distribution exceeding 25g/day rejected | CRUD | Select adult member → enter 26g → submit → quota error toast |
|
||
| 2 | Monthly 50g limit: distribution exceeding 50g/month rejected | CRUD | Select member with 45g used → enter 6g → error toast |
|
||
| 3 | Under-21 THC% limit: distribution of >10% THC to under-21 rejected | CRUD | Select Jonas Weber (U21) → select Amnesia Haze (22% THC) → enter 1g → THC limit error |
|
||
| 4 | Under-21 THC% limit: distribution of ≤10% THC to under-21 allowed | CRUD | Select Jonas Weber → select CBD Critical Mass (0.5% THC) → enter 5g → success |
|
||
| 5 | Under-21 monthly limit: 30g/month (not 50g) | CRUD | Select Jonas Weber → enter 31g of low-THC strain → quota error |
|
||
| 6 | Near-quota member: 23g used + 3g = 26g exceeds daily 25g | CRUD | Select Thomas Müller → enter 3g → error (daily limit) |
|
||
| 7 | Near-quota member: 23g used + 2g = 25g exactly at daily limit | CRUD | Select Thomas Müller → enter 2g → success (exactly at limit) |
|
||
| 8 | Quota display shows correct remaining for near-quota member | Read | Thomas Müller shows "23g / 25g" with warning indicator |
|
||
| 9 | Under-21 member shows THC restriction notice in UI | Read | Jonas Weber's distribution form shows "max. 10% THC" notice |
|
||
|
||
**DB verification after test 7:**
|
||
```typescript
|
||
const quota = await apiClient.getMemberQuota(thomasMuellerId);
|
||
expect(quota.usedToday).toBe(25); // 23 + 2
|
||
expect(quota.remainingToday).toBe(0);
|
||
```
|
||
|
||
---
|
||
|
||
## 4. CI Integration
|
||
|
||
### 4.1 Execution Strategy
|
||
|
||
```yaml
|
||
# In GitHub Actions / Gitea Actions:
|
||
jobs:
|
||
integration-tests:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- name: Start full stack
|
||
run: docker compose -f docker-compose.test.yml up -d --build
|
||
- name: Wait for health
|
||
run: |
|
||
timeout 120 bash -c 'until curl -sf http://localhost:8080/actuator/health; do sleep 3; done'
|
||
timeout 60 bash -c 'until curl -sf http://localhost:3000; do sleep 2; done'
|
||
- name: Run integration tests
|
||
run: |
|
||
docker compose -f docker-compose.test.yml exec playwright \
|
||
npx playwright test e2e/integration/ --reporter=list,html
|
||
- name: Upload HTML report (v2: R-5)
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: playwright-html-report
|
||
path: cannamanage-frontend/playwright-report/
|
||
- name: Upload test artifacts (traces + screenshots)
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: playwright-artifacts
|
||
path: cannamanage-frontend/test-results/
|
||
```
|
||
|
||
### 4.2 Test Tagging Strategy (v2: R-6)
|
||
|
||
Tests are tagged for CI tiering using Playwright's `test.describe` annotations:
|
||
|
||
```typescript
|
||
// Smoke tests — run on every PR push (<2 min)
|
||
test.describe('@smoke', () => {
|
||
test('Dashboard loads', ...);
|
||
test('Login works', ...);
|
||
test('Members table renders', ...);
|
||
});
|
||
|
||
// Full suite — run on main merge + nightly (~8 min)
|
||
test.describe('@full', () => {
|
||
test('Complete CRUD flow', ...);
|
||
test('KCanG regulatory edge cases', ...);
|
||
});
|
||
```
|
||
|
||
**CI trigger mapping:**
|
||
|
||
| Trigger | Tag filter | Timeout |
|
||
|---------|-----------|---------|
|
||
| PR (push) | `--grep @smoke` | 3 min |
|
||
| main merge | (all tests) | 10 min |
|
||
| Nightly | (all tests) + visual regression | 15 min |
|
||
| Manual dispatch | Selectable via `--grep` | Configurable |
|
||
|
||
### 4.3 Flaky Test Handling
|
||
|
||
1. **Retries:** `retries: 1` in CI mode only (via `process.env.CI`)
|
||
2. **Timeouts:** Liberal timeouts for Docker networking (90s test, 60s navigation, 15s expect)
|
||
3. **Wait strategies:** Never use `waitForTimeout` — always wait for specific `data-testid` selectors or network events
|
||
4. **Trace collection:** `trace: 'on-first-retry'` for debugging CI failures
|
||
5. **Screenshot on failure:** `screenshot: 'only-on-failure'` in CI
|
||
|
||
### 4.4 Screenshot Comparison Strategy (v2: R-4)
|
||
|
||
**Approach: Structural comparison, NOT pixel-diff.**
|
||
|
||
Pixel-diff is fragile across environments (font rendering, anti-aliasing, Docker vs local). Instead:
|
||
- Use Playwright's `toHaveScreenshot()` with `maxDiffPixelRatio: 0.01` tolerance
|
||
- Store baseline screenshots in `e2e/integration/__screenshots__/` (committed to git)
|
||
- Update baselines via `npx playwright test --update-snapshots` when UI intentionally changes
|
||
- In CI: compare against committed baselines; fail on structural regressions
|
||
|
||
For the nightly visual regression run:
|
||
```typescript
|
||
test('Dashboard visual regression @nightly', async ({ page }) => {
|
||
await page.goto('/dashboard');
|
||
await expect(page).toHaveScreenshot('dashboard.png', { maxDiffPixelRatio: 0.01 });
|
||
});
|
||
```
|
||
|
||
### 4.5 Artifact Collection
|
||
|
||
On failure, collect:
|
||
- Screenshots (per-test on failure)
|
||
- Playwright trace files (`.zip` — viewable via trace.playwright.dev)
|
||
- Backend logs: `docker compose logs backend > backend.log`
|
||
- **HTML report:** `--reporter=html` generates a browsable report (uploaded as CI artifact, viewable directly)
|
||
|
||
---
|
||
|
||
## 5. Docker Compose Improvements
|
||
|
||
### 5.1 Updated `docker-compose.test.yml` (v3)
|
||
|
||
```yaml
|
||
services:
|
||
db:
|
||
image: postgres:16-alpine
|
||
container_name: cannamanage-test-db
|
||
environment:
|
||
POSTGRES_DB: cannamanage_test
|
||
POSTGRES_USER: cannamanage
|
||
POSTGRES_PASSWORD: testpass
|
||
# v3: tmpfs for CI speed (ephemeral data). For macOS local dev, use docker-compose.test.local.yml override.
|
||
tmpfs:
|
||
- /var/lib/postgresql/data
|
||
# v2: NO volume mount for init.sql — seed handled by Flyway R__seed_test_data.sql
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U cannamanage"]
|
||
interval: 3s
|
||
timeout: 3s
|
||
retries: 10
|
||
|
||
backend:
|
||
build:
|
||
context: .
|
||
dockerfile: Dockerfile.backend
|
||
container_name: cannamanage-test-backend
|
||
environment:
|
||
SPRING_PROFILES_ACTIVE: docker,test
|
||
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/cannamanage_test
|
||
SPRING_DATASOURCE_USERNAME: cannamanage
|
||
SPRING_DATASOURCE_PASSWORD: testpass
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "curl -sf http://localhost:8080/actuator/health || exit 1"]
|
||
interval: 5s
|
||
timeout: 5s
|
||
retries: 20
|
||
|
||
frontend:
|
||
build:
|
||
context: ./cannamanage-frontend
|
||
dockerfile: Dockerfile
|
||
container_name: cannamanage-test-frontend
|
||
environment:
|
||
NEXT_PUBLIC_API_URL: http://backend:8080
|
||
depends_on:
|
||
backend:
|
||
condition: service_healthy
|
||
|
||
playwright:
|
||
build:
|
||
context: ./cannamanage-frontend
|
||
dockerfile: Dockerfile.playwright
|
||
container_name: cannamanage-test-playwright
|
||
environment:
|
||
BASE_URL: http://frontend:3000
|
||
API_URL: http://backend:8080
|
||
CI: "true"
|
||
depends_on:
|
||
frontend:
|
||
condition: service_started
|
||
backend:
|
||
condition: service_healthy
|
||
# v3 (v2-1): Volume mounts OVERRIDE the COPY'd files from Dockerfile.playwright at runtime.
|
||
# This is intentional — the Dockerfile pre-installs deps (node_modules), while
|
||
# the volume mounts allow iterating on test code without rebuilding the image.
|
||
# The e2e/ and config mounts are :ro (read-only); results/report are writable for output.
|
||
volumes:
|
||
- ./cannamanage-frontend/e2e:/app/e2e:ro
|
||
- ./cannamanage-frontend/playwright.config.ts:/app/playwright.config.ts:ro
|
||
- ./cannamanage-frontend/test-results:/app/test-results
|
||
- ./cannamanage-frontend/playwright-report:/app/playwright-report
|
||
command: >
|
||
sh -c "
|
||
echo 'Waiting for frontend...' &&
|
||
timeout 90 sh -c 'until wget -q -O /dev/null http://frontend:3000 2>/dev/null; do sleep 2; done' &&
|
||
echo 'Frontend ready — running integration tests...' &&
|
||
npx playwright test e2e/integration/ --reporter=list,html
|
||
"
|
||
```
|
||
|
||
### 5.2 Playwright Dockerfile (v3: A-4 + v2-3)
|
||
|
||
**New file: `cannamanage-frontend/Dockerfile.playwright`**
|
||
|
||
The Playwright container needs its own Dockerfile to pre-install dependencies (panel A-4).
|
||
|
||
> **⚠️ Version Pinning Rule (v3: v2-3):** The Playwright Docker image version (`v1.49.0` below) MUST always match the `@playwright/test` version in `package.json`. A mismatch between the Docker image (which bundles browser binaries) and the npm package (which provides the API) causes cryptic browser launch failures. When upgrading Playwright, update BOTH `package.json` AND this Dockerfile in the same commit.
|
||
|
||
```dockerfile
|
||
# IMPORTANT: Keep this version in sync with @playwright/test in package.json (v3: v2-3)
|
||
FROM mcr.microsoft.com/playwright:v1.49.0-jammy
|
||
|
||
WORKDIR /app
|
||
|
||
# Copy package files for dependency install
|
||
COPY package.json pnpm-lock.yaml .npmrc ./
|
||
|
||
# Install pnpm and dependencies (v2: A-4 — pre-install deps)
|
||
RUN npm install -g pnpm && \
|
||
pnpm install --frozen-lockfile
|
||
|
||
# Copy Playwright config and test sources
|
||
COPY playwright.config.ts ./
|
||
COPY e2e/ ./e2e/
|
||
|
||
# Playwright browsers are pre-installed in the base image
|
||
```
|
||
|
||
This ensures `pnpm install --frozen-lockfile` runs at build time, not at test runtime.
|
||
|
||
### 5.5 Local Development Override (v3: A-2 — tmpfs conditional)
|
||
|
||
**New file: `docker-compose.test.local.yml`**
|
||
|
||
On macOS with Docker Desktop, `tmpfs` may cause Postgres startup failures due to the Linux VM's handling of tmpfs syscalls. For local development, use a named volume instead:
|
||
|
||
```yaml
|
||
# docker-compose.test.local.yml — override for macOS local development
|
||
# Usage: docker compose -f docker-compose.test.yml -f docker-compose.test.local.yml up
|
||
services:
|
||
db:
|
||
tmpfs: [] # disable tmpfs from base compose
|
||
volumes:
|
||
- test-db-data:/var/lib/postgresql/data
|
||
|
||
volumes:
|
||
test-db-data:
|
||
driver: local
|
||
```
|
||
|
||
**When to use which:**
|
||
|
||
| Environment | Command | DB Storage |
|
||
|-------------|---------|-----------|
|
||
| CI (GitHub Actions, `ubuntu-latest`) | `docker compose -f docker-compose.test.yml up` | `tmpfs` (fast, ephemeral) |
|
||
| Local macOS (Docker Desktop) | `docker compose -f docker-compose.test.yml -f docker-compose.test.local.yml up` | Named volume (compatible) |
|
||
|
||
Alternatively, developers who don't experience tmpfs issues on their Docker Desktop version can use the base compose directly. The override is opt-in for those who hit Postgres startup failures.
|
||
|
||
**CI detection:** The CI workflow uses only `docker-compose.test.yml` (tmpfs enabled by default). The `CI=true` environment variable is already set in the playwright service. No conditional logic needed in the compose file itself — the override file approach keeps it simple and declarative.
|
||
|
||
### 5.3 Key Improvements (v2 summary)
|
||
|
||
| Change | Reason | Panel Finding |
|
||
|--------|--------|---------------|
|
||
| Remove `init.sql` volume mount from db | Seed timing contradiction — Flyway handles it | A-1 (BLOCKER) |
|
||
| `tmpfs` for Postgres | 3-5x faster writes, data is ephemeral | — |
|
||
| `test` Spring profile | Gates `/api/v1/test/reset-db` + Flyway testdata location | A-3 |
|
||
| Explicit `container_name` per service | Clear network addressing (A-2) | A-2 |
|
||
| `Dockerfile.playwright` with `pnpm install` | Pre-install deps at build time | A-4 |
|
||
| Health check on backend before playwright starts | Prevents flaky first-test failures | A-5 |
|
||
| `--reporter=html` + artifact upload | Browsable CI reports | R-5 |
|
||
| Frontend accessible as `http://frontend:3000` | Docker service name = hostname for Playwright | A-2 |
|
||
|
||
### 5.4 Network Addressing (v2: A-2)
|
||
|
||
Docker Compose service names serve as DNS hostnames within the compose network:
|
||
- **Frontend** is reachable at `http://frontend:3000` from the `playwright` container
|
||
- **Backend** is reachable at `http://backend:8080` from both `frontend` and `playwright`
|
||
- **Database** is reachable at `db:5432` from `backend`
|
||
|
||
The `BASE_URL` environment variable for Playwright is set to `http://frontend:3000`. No custom network aliases or extra configuration needed — Docker Compose default network handles it.
|
||
|
||
---
|
||
|
||
## 6. Implementation Phases
|
||
|
||
### Phase 2A: Seed Data Expansion + Flyway Integration (Day 1)
|
||
1. Create `cannamanage-api/src/main/resources/db/testdata/R__seed_test_data.sql`
|
||
2. Expand with all missing entities (staff/member users, documents, events, forum, grow, compliance)
|
||
3. Add KCanG edge case seed data: under-21 member (Jonas Weber), near-quota member (Thomas Müller, 23g/25g)
|
||
4. Add `spring.flyway.locations` to `application-test.properties`
|
||
5. Verify: start backend with `--spring.profiles.active=test` → seed data present in DB
|
||
|
||
### Phase 2B: Backend Test Profile + Reset Endpoint (Day 1)
|
||
1. Implement `TestResetController` with `POST /api/v1/test/reset-db`
|
||
2. Gate with `@Profile("test")` annotation
|
||
3. Controller executes TRUNCATE CASCADE + re-runs seed SQL via JdbcTemplate
|
||
4. Add integration test for the reset endpoint itself
|
||
5. Verify: call endpoint → DB returns to seed state
|
||
|
||
### Phase 2C: Test Infrastructure + data-testid Attributes (Day 2)
|
||
1. Create `e2e/integration/helpers/api-client.ts` (with `resetDb()` method)
|
||
2. Create `e2e/integration/helpers/db-reset.ts`
|
||
3. Create `e2e/integration/helpers/selectors.ts` (data-testid constants)
|
||
4. Update `playwright.config.ts` with `integration` project (incl. `expect.timeout: 15_000`)
|
||
5. Update `global-setup.ts` with health check wait loop
|
||
6. **Add `data-testid` attributes to all frontend components** being tested (critical sub-task)
|
||
7. Create `cannamanage-frontend/Dockerfile.playwright`
|
||
8. Update `docker-compose.test.yml` (remove init.sql mount, add Dockerfile.playwright, add health checks)
|
||
|
||
### Phase 2D: Integration Test Specs (Day 2-4)
|
||
1. Priority 1: Members, Distributions, Stock (core business logic)
|
||
2. Priority 2: Documents, Board (recently fixed in Sprint 12 Phase 1)
|
||
3. Priority 3: Calendar, Forum, Info Board (communication)
|
||
4. Priority 4: Finance, Grow, Compliance (supporting modules)
|
||
5. Priority 5: **KCanG Regulatory (`13-kcang-regulatory.spec.ts`)** — daily 25g limit, under-21 THC%, monthly quotas
|
||
|
||
### Phase 2E: CI Pipeline (Day 4)
|
||
1. Create GitHub/Gitea Actions workflow with `@smoke` / `@full` tagging
|
||
2. Configure artifact upload (HTML report + traces + screenshots)
|
||
3. Add visual regression baseline screenshots
|
||
4. Test full cycle: push → build → test → report
|
||
|
||
---
|
||
|
||
## 7. Success Criteria
|
||
|
||
| Criterion | Metric |
|
||
|-----------|--------|
|
||
| All seed data loads without errors | Backend starts with `test` profile, no Flyway errors |
|
||
| Integration tests pass against real DB | 70+ test cases (incl. 9 KCanG regulatory), 100% pass rate |
|
||
| Per-test DB reset works | Each test starts from identical seed state |
|
||
| Test execution time (v3: R-1) | `@full` suite < 8 minutes; `@smoke` suite < 2 minutes |
|
||
| No flaky tests on 3 consecutive runs | 3/3 green runs |
|
||
| CI pipeline works end-to-end | PR triggers → results posted with HTML report |
|
||
| DB state verification works | API assertions confirm CRUD effects |
|
||
| data-testid selectors stable | No selector-based failures across runs |
|
||
| KCanG regulatory tests pass | All 9 edge cases correctly enforced |
|
||
| seed-constants.ts consistency (v3: R-4) | All assertions import from `seed-constants.ts`, no hardcoded seed values in specs |
|
||
|
||
---
|
||
|
||
## 8. Risks & Mitigations
|
||
|
||
| Risk | Probability | Impact | Mitigation |
|
||
|------|-------------|--------|-----------|
|
||
| Flyway repeatable migration not re-running on unchanged checksum | Low | High | Document: change seed content → checksum changes → Flyway re-runs automatically |
|
||
| Per-test reset too slow (>500ms × 70 tests = 35s overhead) | Low | Low | Acceptable — reset is <500ms, total overhead ~35s for full isolation |
|
||
| Test data coupling (tests depend on specific IDs) | Medium | Medium | Use deterministic UUIDs, document in selectors.ts |
|
||
| Docker build time in CI | Low | Medium | Cache Docker layers, use pre-built Playwright image |
|
||
| Frontend hydration issues with real data | Low | High | Use `waitForLoadState('networkidle')` + `data-testid` selectors |
|
||
| Backend API response format changes break tests | Medium | Medium | Use API client abstraction, update in one place |
|
||
| data-testid attributes missing in new components | Medium | Medium | Enforce via PR review checklist + ESLint rule (future) |
|
||
| First-test warmup causing timeout | Low | Low | `expect.timeout: 15_000` accommodates warmup (panel R-3) |
|
||
| Connection pool warmup on first request | Low | Low | globalSetup calls resetDb() which warms the pool before tests run |
|
||
|
||
---
|
||
|
||
## 9. Resolved Questions (v2)
|
||
|
||
These were open questions in v1, now resolved per panel review:
|
||
|
||
| Question | Resolution | Panel Finding |
|
||
|----------|-----------|---------------|
|
||
| Should we add `data-testid` attributes? | **YES — mandatory.** All testable elements get `data-testid`. Naming: `<page>-<component>-<identifier>` | R-2 |
|
||
| Member portal integration test suite? | Yes — separate auth state file (`member.json`), used by portal-specific tests | — |
|
||
| CRUD tests cleanup vs dirty state? | **Per-test reset via API endpoint.** Each test starts clean. | R-1 |
|
||
| Preferred CI platform? | GitHub Actions (primary), Gitea Actions (mirror) | — |
|
||
| Seed data loading timing? | **Flyway-only via `R__seed_test_data.sql`** in `classpath:db/testdata`. No Docker init.sql mount. | A-1, A-3 |
|
||
| Screenshot comparison strategy? | Structural comparison with `maxDiffPixelRatio: 0.01` — NOT pixel-diff | R-4 |
|
||
| CI reporter artifact strategy? | `--reporter=html` uploaded as CI artifact | R-5 |
|
||
| Test tiering for CI? | `@smoke` (PR) / `@full` (merge + nightly) via `--grep` | R-6 |
|
||
|
||
---
|
||
|
||
## Appendix A: Panel Review Changes Summary (v1 → v2)
|
||
|
||
| Finding | Severity | Resolution in v2 |
|
||
|---------|----------|-----------------|
|
||
| A-1: Seed timing contradiction | ❌ BLOCKER | Removed Docker init.sql mount; Flyway-only seeding |
|
||
| D-1: Missing KCanG 25g/day limit tests | ⚠️ | Added `13-kcang-regulatory.spec.ts` with 9 test cases |
|
||
| D-2: Missing under-21 THC% test | ⚠️ | Included in KCanG spec (tests 3, 4, 5, 9) |
|
||
| D-3: Near-quota member in seed | ℹ️ | Added Thomas Müller (23g/25g) to seed data |
|
||
| A-2: Network alias clarity | ⚠️ | Explicit container_name + documented service-name networking |
|
||
| A-3: Flyway location specification | ⚠️ | `db/testdata/R__seed_test_data.sql` + `application-test.properties` |
|
||
| A-4: Playwright pnpm install | ⚠️ | Custom `Dockerfile.playwright` with pre-installed deps |
|
||
| A-5: Health check wait in globalSetup | ℹ️ | Added `waitForUrl` in globalSetup before any tests |
|
||
| R-1: Per-test DB reset | ⚠️ | `beforeEach` calls `apiClient.resetDb()` |
|
||
| R-2: data-testid commitment | ⚠️ | Mandatory — naming convention + selectors.ts defined |
|
||
| R-3: expect.timeout + warmup | ⚠️ | `expect.timeout: 15_000` + documented warmup behavior |
|
||
| R-4: Screenshot comparison | ℹ️ | Structural (maxDiffPixelRatio), not pixel-diff |
|
||
| R-5: HTML reporter artifacts | ℹ️ | `--reporter=html` + CI artifact upload |
|
||
| R-6: Test tagging | ℹ️ | `@smoke` / `@full` with `--grep` in CI |
|
||
|
||
---
|
||
|
||
## Appendix B: v2 Re-Review Changes Summary (v2 → v3)
|
||
|
||
Changes made to address partially-resolved and info findings from the v2 re-review:
|
||
|
||
| Finding | Severity | Resolution in v3 |
|
||
|---------|----------|-----------------|
|
||
| A-2: tmpfs unconditional — no CI-only gating | ⚠️ Partial | Added `docker-compose.test.local.yml` override with named volume for macOS. Documented CI vs local usage in Section 5.5. |
|
||
| R-1: "< 5 minutes" success criterion optimistic | ⚠️ Partial | Changed to "< 8 minutes for `@full` suite, < 2 minutes for `@smoke` suite" in Section 7. |
|
||
| R-4: No explicit seed-constants.ts | ⚠️ Partial | Added complete `seed-constants.ts` file with all deterministic UUIDs, member data, KCanG limits, and counts (Section 2.8). Rule: never hardcode seed values in specs. |
|
||
| D-4: `recorded_by` should reference admin UUID | ℹ️ Info | Updated seed data design — destruction records and distributions use admin UUID `b1000000-...001` as `recorded_by`. |
|
||
| v2-1: Volume mount + Dockerfile build overlap | ℹ️ Info | Added explanatory comment in docker-compose.test.yml Section 5.1 documenting the intentional pattern (Dockerfile pre-installs deps, volume mounts allow test iteration). |
|
||
| v2-3: Playwright Docker image version pinning | ℹ️ Info | Added version pinning rule in Section 5.2: Playwright image version MUST match `@playwright/test` in package.json. Comment added to Dockerfile. |
|