docs: Sprint 12 planning, analysis, reviews, and code review

- 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)
This commit is contained in:
Patrick Plate
2026-06-18 14:43:25 +02:00
parent 776149e7d3
commit be932c1930
6 changed files with 2436 additions and 0 deletions
@@ -0,0 +1,144 @@
# Sprint 12 Analysis: "Golden Test Standard — Everything Present Must Work"
**Datum:** 18.06.2026
**Autor:** Patrick Plate / Lumen (Planner)
**Status:** v1
**Prinzip:** Jeder sichtbare Button muss funktionieren — sonst warum ist er da?
---
## 1. Audit-Zusammenfassung
| Seite | Status | P0 (broken) | P1 (UX) | P2 (polish) |
|-------|--------|-------------|----------|-------------|
| `/documents` | ❌ Kritisch | 3 | 2 | 0 |
| `/board` | ❌ Kritisch | 3 | 0 | 0 |
| `/dashboard` | ✅ OK | 0 | 0 | 0 |
| `/members` | ✅ OK | 0 | 0 | 0 |
| `/distributions` | ✅ OK | 0 | 0 | 0 |
| `/stock` | ✅ OK | 0 | 0 | 0 |
| `/grow` | ✅ OK | 0 | 0 | 0 |
| `/reports` | ✅ OK | 0 | 0 | 0 |
| `/calendar` | ✅ OK | 0 | 0 | 0 |
| `/forum` | ✅ OK | 0 | 0 | 0 |
| `/info-board` | ✅ OK | 0 | 0 | 0 |
| `/finance` | ✅ OK | 0 | 0 | 0 |
| `/assemblies` | ✅ OK | 0 | 0 | 0 |
| `/compliance` | ✅ OK | 0 | 0 | 0 |
| `/audit-log` | ✅ OK | 0 | 0 | 0 |
| `/settings/staff` | ✅ OK | 0 | 0 | 0 |
| `/settings/billing` | ⚠️ TBD | 0 | 0 | 0 |
| `/settings/privacy` | ⚠️ TBD | 0 | 0 | 0 |
**Gesamt: 6 P0-Defekte, 2 P1-Defekte** — alle konzentriert auf 2 Seiten.
---
## 2. P0 Findings — Komplett defekte Buttons
### 2.1 Documents Page (`documents/page.tsx`)
| # | Element | Zeile | Problem | Service vorhanden? |
|---|---------|-------|---------|-------------------|
| P0-1 | Upload-Button im Dialog | 217 | `onClick={() => setUploadOpen(false)}` — schließt nur den Dialog, ruft nie `uploadDocument()` auf | ✅ Ja: `uploadDocument()` in `services/documents.ts:31` |
| P0-2 | Download-Button pro Datei | 307-308 | `<Button variant="ghost" size="icon">`**kein onClick handler** | ✅ Ja: `downloadDocument()` in `services/documents.ts:73` |
| P0-3 | Delete-Button pro Datei | 310-313 | `<Button variant="ghost" size="icon">`**kein onClick handler** | ✅ Ja: `deleteDocument()` in `services/documents.ts:79` |
**Root Cause:** Die Documents-Seite wurde als statische Mock-Darstellung gebaut. Die Service-Schicht (`services/documents.ts`) existiert vollständig mit `uploadDocument()`, `downloadDocument()`, `deleteDocument()` und `listDocuments()` — aber keine dieser Funktionen wird von der Page aufgerufen. Die Seite nutzt nicht mal React Query (nur `useState` mit hartkodierten Mock-Daten).
### 2.2 Board Page (`board/page.tsx`)
| # | Element | Zeile | Problem | Service vorhanden? |
|---|---------|-------|---------|-------------------|
| P0-4 | "Position speichern" Button | 189-191 | `onClick={() => setPositionDialogOpen(false)}` — schließt nur Dialog, kein API-Call | ✅ Ja: `createPosition()` in `services/board.ts` |
| P0-5 | "Wahl bestätigen" Button | 244-248 | `onClick={() => setElectDialogOpen(false)}` — schließt nur Dialog, kein API-Call | ✅ Ja: `electBoardMember()` in `services/board.ts` |
| P0-6 | Mitglied absetzen (UserMinus) | 269-273 | `<Button variant="ghost" size="icon">`**kein onClick handler** | ✅ Ja: `removeBoardMember()` in `services/board.ts` |
**Root Cause:** Identisches Pattern wie Documents — die Seite zeigt Mock-Daten, die Dialoge sammeln Formulardaten, aber der Submit-Button schließt nur den Dialog ohne die Service-Schicht aufzurufen.
---
## 3. P1 Findings — UX-Issues (Documents speziell)
### 3.1 Documents: Fehlende visuelle Kategorie-Unterscheidung
**Problem:** Die `getCategoryBadgeVariant()` Funktion nutzt nur 4 generische Badge-Varianten (`default`, `secondary`, `destructive`, `outline`) für 6 Kategorien. Mehrere Kategorien teilen sich denselben Stil:
- VERTRAG und VERSICHERUNG → beide `outline` (identisch)
- PROTOKOLL und SONSTIGES → beide `secondary` (identisch)
**Lösung:** Eigene Farben pro Kategorie + Icons:
- SATZUNG → Blau + BookOpen-Icon
- PROTOKOLL → Lila + FileText-Icon
- VERTRAG → Amber + FileSignature-Icon
- VERSICHERUNG → Cyan + Shield-Icon
- GENEHMIGUNG → Grün + CheckCircle-Icon
- SONSTIGES → Grau + File-Icon
### 3.2 Documents: Table-Layout ohne min-width
**Problem:** Die Table-Zellen haben keine `min-w-*` oder `w-*` Constraints. Bei langen Dateinamen/Titeln stretcht sich die Name-Spalte über die gesamte verfügbare Breite, während kurze Zellen (Size, Date) zusammengedrückt werden.
**Lösung:**
- Name-Spalte: `max-w-[300px] truncate`
- Access-Spalte: `w-[120px]`
- Size-Spalte: `w-[80px]`
- Date-Spalte: `w-[100px]`
- Actions-Spalte: `w-[80px]`
---
## 4. Funktionale Seiten (kein Handlungsbedarf)
Die folgenden Seiten sind korrekt mit ihren Service-Layern verdrahtet:
| Seite | Pattern | Mutations/Actions |
|-------|---------|-------------------|
| Members | React Query + TanStack Table | Edit navigiert zu `/members/[id]` ✅ |
| Distributions | `useDistributionsQuery()` + TanStack Table | "New" verlinkt korrekt ✅ |
| Stock | `useBatchesQuery()` + `useRecallBatchMutation()` | Recall mit AlertDialog ✅ |
| Reports | `useMonthlyReportQuery()` etc. | Download-Buttons mit try/catch + Blob ✅ |
| Calendar | `useEventsQuery()` + `useCreateEventMutation()` + `useCancelEventMutation()` | Create-Form + Cancel funktional ✅ |
| Forum | `useCreateTopic()` + `useLockTopic()` etc. | Alle Moderation-Buttons verdrahtet ✅ |
| Info-Board | `useCreatePostMutation()` + `useDeletePostMutation()` etc. | CRUD komplett ✅ |
| Finance | `useRecordPaymentMutation()` + `useRecordExpenseMutation()` | PaymentForm/ExpenseForm mit `onSubmit` ✅ |
| Assemblies | `createAssembly()` + `getAssemblies()` | Create-Dialog mit Service-Call ✅ |
| Compliance | `getComplianceDashboard()` + `completeDeadline()` | Deadline-Buttons funktional ✅ |
| Audit-Log | `useAuditLogQuery()` + `useExportAuditPdfMutation()` | Export mit Loading-State ✅ |
| Staff | `useInviteStaffMutation()` + `useRevokeStaffMutation()` | Invite/Revoke/Permissions komplett ✅ |
---
## 5. Architektur-Analyse: Documents Page Refactoring
Die Documents-Seite braucht ein komplettes Refactoring von "static mock display" zu "React Query powered":
**Ist-Zustand:**
```
useState(mockDocuments) → static render → buttons do nothing
```
**Soll-Zustand:**
```
useQuery(['documents']) → dynamic data
useMutation(uploadDocument) → upload with progress
downloadDocument(id) → blob download + save-as
useMutation(deleteDocument) → confirm dialog + optimistic update
```
**Vorhandene Service-Funktionen (alle bereits implementiert in `services/documents.ts`):**
- `listDocuments(clubId, category?, accessLevel?)``GET /documents`
- `uploadDocument(clubId, title, category, accessLevel, description, file)``POST /documents/upload` (multipart)
- `downloadDocument(id)``GET /documents/{id}/download` → Blob
- `deleteDocument(id, clubId)``DELETE /documents/{id}`
- `getStorageUsage(clubId)``GET /documents/usage`
---
## 6. Empfehlung
**Scope ist sehr fokussiert:** Nur 2 Seiten brauchen Arbeit — Documents und Board. Alle anderen 16+ Seiten sind funktional korrekt verdrahtet.
Geschätzter Aufwand:
- Documents Page Refactoring: ~3h (React Query integration + download/delete handlers + UX fixes)
- Board Page Wiring: ~1.5h (3 handlers verdrahten + Confirmation-Dialogs)
- **Gesamt: ~4.5h**
@@ -0,0 +1,194 @@
# Code Review: CannaManage Sprint 12
**Date:** 18.06.2026
**Reviewer:** Roo (Reviewer)
**Branch:** main (uncommitted working tree changes)
**Status:** ⚠️ Approved with comments — 2 blockers, 4 warnings, 3 suggestions
---
## Summary
Sprint 12 delivers two phases: (1) Documents + Board page rewrites with React Query integration and dual-mode pattern, (2) Full integration test infrastructure with Docker, Playwright, and deterministic seed data. The architecture is solid and patterns are consistent. However, there are **2 blockers** that will prevent tests from running: a credential mismatch in global-setup.ts and missing `data-testid` attributes in the actual components.
## Changed Files
| File | Change | Rating |
|------|--------|--------|
| `cannamanage-frontend/src/services/documents.ts` | Modified | ✅ |
| `cannamanage-frontend/src/services/board.ts` | New | ✅ |
| `cannamanage-frontend/src/app/(dashboard-layout)/documents/page.tsx` | Modified | ⚠️ |
| `cannamanage-frontend/src/app/(dashboard-layout)/board/page.tsx` | Modified | ⚠️ |
| `cannamanage-api/.../TestResetController.java` | New | ✅ |
| `cannamanage-api/.../application-test.properties` | New | ⚠️ |
| `cannamanage-api/.../db/testdata/R__seed_test_data.sql` | New | ✅ |
| `docker-compose.test.yml` | Modified | ⚠️ |
| `docker-compose.test.local.yml` | New | ✅ |
| `cannamanage-frontend/Dockerfile.playwright` | New | ✅ |
| `cannamanage-frontend/playwright.config.ts` | Modified | ✅ |
| `cannamanage-frontend/e2e/global-setup.ts` | Modified | ❌ |
| `cannamanage-frontend/e2e/seed-constants.ts` | New | ✅ |
| `cannamanage-frontend/e2e/selectors.ts` | New | ⚠️ |
| `cannamanage-frontend/e2e/api-client.ts` | New | ✅ |
| `cannamanage-frontend/e2e/integration/*.spec.ts` (13 files) | New | ✅ |
## Checklist
| # | Check | Result | Notes |
|---|-------|--------|-------|
| 1 | Plan compliance | ✅ | All plan items implemented |
| 2 | React patterns | ✅ | Proper React Query mutations, `onSuccess`/`onError` handlers, query invalidation |
| 3 | TypeScript | ✅ | No `any` types in new code (except `api-client.ts` — see W-4), proper interfaces |
| 4 | Error handling | ✅ | try/catch on async ops, toast feedback consistent |
| 5 | Security | ✅ | TestResetController properly gated by `@ConditionalOnProperty` |
| 6 | SQL correctness | ✅ | `ON CONFLICT DO NOTHING` throughout, valid PostgreSQL |
| 7 | Docker | ⚠️ | Credential mismatches between properties and compose (W-1) |
| 8 | Test quality | ⚠️ | Good patterns overall, but `data-testid` attrs missing (B-2) |
| 9 | i18n | ⚠️ | Several hardcoded German strings in AlertDialogs (W-2) |
| 10 | Accessibility | ✅ | Buttons have proper labels, form inputs have Label components |
| 11 | Mock mode | ✅ | Dual-mode pattern is consistent across both pages |
| 12 | Build passes | ⏭️ | Not verified in review (no local run) |
---
## Findings
### ❌ Blockers (must fix before merge)
#### B-1: Login credentials mismatch between global-setup and seed data
**[`cannamanage-frontend/e2e/global-setup.ts`](cannamanage-frontend/e2e/global-setup.ts:46)**
The global-setup authenticates with:
```typescript
await page.fill('input[name="email"]...', "admin@test.de")
await page.fill('input[name="password"]...', "test123")
```
But the seed data creates the admin user with:
- Email: `admin@gruener-daumen.de` (in `R__seed_test_data.sql`)
- Password: `TestAdmin123!` (in `seed-constants.ts`)
This means **all integration tests will fail at the setup phase** because authentication will not succeed against the seeded database.
**Fix:** Update `global-setup.ts` to use `SEED.admin.email` and `SEED.admin.password`:
```typescript
import { SEED } from "./seed-constants"
await page.fill('...', SEED.admin.email)
await page.fill('...', SEED.admin.password)
```
---
#### B-2: `data-testid` attributes not present in components
**[`cannamanage-frontend/e2e/selectors.ts`](cannamanage-frontend/e2e/selectors.ts:8)**
The `SEL` object defines selectors like `[data-testid="documents-upload-button"]`, `[data-testid="documents-download-f1000000..."]`, etc. However, the actual components in `documents/page.tsx` and `board/page.tsx` **do not have any `data-testid` attributes**.
All spec files using `page.locator(SEL.documents.uploadButton)` etc. will fail with element-not-found errors.
**Note:** The `selectors.ts` file itself comments (line 7): _"The actual data-testid attributes will be added incrementally to frontend components during Phase 2E as tests are written."_ — but this Phase 2E work was not done yet. Either:
1. Add the `data-testid` attributes to the components now, or
2. Rewrite specs to use accessible selectors (role, label, text) instead
---
### ⚠️ Warnings (should fix)
#### W-1: Credential/secret mismatch between application-test.properties and docker-compose.test.yml
**[`cannamanage-api/src/main/resources/application-test.properties`](cannamanage-api/src/main/resources/application-test.properties:9)** vs **[`docker-compose.test.yml`](docker-compose.test.yml:26)**
| Setting | application-test.properties | docker-compose.test.yml |
|---------|---------------------------|------------------------|
| DB username | `cannamanage_test` | `cannamanage` |
| DB password | `test_password` | `cannamanage_test` |
| JWT secret | `...LXRlc3RzLW9ubHktMzJi` | `...LXRlc3RzLW9ubHktMzJjaGFycw==` |
In Docker, env vars override properties, so Docker-based tests will work. But anyone running `./mvnw test -Ptest` locally against the Docker-compose DB will hit authentication failures.
**Fix:** Align the properties file with the Docker compose values, or add a comment explaining the intentional override.
---
#### W-2: Hardcoded German strings in AlertDialogs (i18n gap)
**[`cannamanage-frontend/src/app/(dashboard-layout)/documents/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/documents/page.tsx:568)**
Multiple hardcoded strings bypass the i18n system:
- Line 447: `"Wird hochgeladen..."`
- Line 568: `"Dokument löschen?"`
- Line 570: `"Möchtest du...wirklich löschen?"`
- Line 575: `"Abbrechen"`
- Line 580: `"Löschen..."` / `"Löschen"`
Same pattern in `board/page.tsx`:
- Line 400: `"Wird gespeichert..."`
- Line 587: `"Vorstandsmitglied entfernen?"`
- Lines 593599: `"Abbrechen"`, `"Entfernen..."`
**Fix:** Use `t("confirmDelete")`, `t("cancel")`, etc. from the `useTranslations` hook already imported.
---
#### W-3: Board page member select shows hardcoded mock options in API mode
**[`cannamanage-frontend/src/app/(dashboard-layout)/board/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/board/page.tsx:439)**
The "Elect Member" dialog always shows only 3 hardcoded `<option>` elements:
```tsx
<option value="m1">Max Mustermann</option>
<option value="m2">Anna Schmidt</option>
<option value="m3">Peter Weber</option>
```
This is acceptable for mock mode, but should load real members from the API when not in mock mode.
---
#### W-4: `any` return types in api-client.ts
**[`cannamanage-frontend/e2e/api-client.ts`](cannamanage-frontend/e2e/api-client.ts:29)**
All public methods return `Promise<any>`. Since this is test infrastructure, it's less critical, but typed responses would make spec assertions more robust.
---
### ️ Suggestions (nice to have)
#### S-1: global-setup.ts uses `waitForTimeout(1000)`
**[`cannamanage-frontend/e2e/global-setup.ts`](cannamanage-frontend/e2e/global-setup.ts:57)**
While generally acceptable for auth state setup, consider replacing with `waitForLoadState("networkidle")` for more deterministic behavior.
---
#### S-2: Docker compose frontend service missing healthcheck
**[`docker-compose.test.yml`](docker-compose.test.yml:40)**
The `frontend` service uses `condition: service_started` but has no healthcheck defined. The Playwright container compensates with a wget loop (line 7677), but a proper healthcheck would be cleaner and allow `condition: service_healthy`.
---
#### S-3: Consider extracting shared mock data to a constants file
Both `documents/page.tsx` (lines 69135) and `board/page.tsx` (lines 42158) contain substantial mock data inline. Consider moving these to `src/data/mock/documents.ts` and `src/data/mock/board.ts` to match the existing pattern (`src/data/mock/dashboard.ts`, `src/data/mock/members.ts`, etc.).
---
## Architecture Highlights (Positive)
- **Dual-mode pattern** is well-executed: `const isMockMode = !data` → graceful fallback, operations work locally without backend
- **TestResetController** uses `@ConditionalOnProperty` — correct Spring Boot pattern, impossible to accidentally activate in prod
- **Repeatable migration** (`R__seed_test_data.sql`) with `ON CONFLICT DO NOTHING` — properly idempotent
- **Playwright 3-project config** (setup → authenticated → integration) is a solid pattern
- **Seed constants as single source of truth** — avoids magic strings in specs
- **Docker tmpfs for test DB** — fast, ephemeral, no disk pollution
- **ApiClient with DB reset in `beforeEach`** — ensures test isolation
## Recommendation
**⚠️ Approved with comments** — Fix the 2 blockers (B-1 credential mismatch, B-2 missing data-testid attributes) before running integration tests. The i18n warnings (W-2) and credential alignment (W-1) should be addressed before final merge but are not blocking functionality.
@@ -0,0 +1,999 @@
# 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. |
@@ -0,0 +1,417 @@
# Expert Panel Review: Sprint 12 Phase 2 — Integration Tests with Seed DB
**Datum:** 2026-06-18
**Artifact:** `docs/sprint-12/cannamanage-sprint12-phase2-integration-tests.md` (v1)
**Panel:** Domain Expert 🏛️ | Architecture Expert 🔧 | Risk & Reliability Expert 🛡️
**Ticket:** CANNAMANAGE-SPRINT12
---
## 🏛️ Expert 1: Domain Expert (Cannabis Club Regulatory Compliance / KCanG)
### Assessment: ⚠️ Mostly Sound — 2 Gaps
#### Strengths
1. **Under-21 quota testing is present** — Test case 3.3 #4 explicitly tests the 30g/month limit for under-21 members (Jonas Weber, `is_under_21=true`). This is KCanG §3 Abs. 2 critical.
2. **Destruction records in seed data** — Plan includes compliance audit trail (V23 `destruction_records`), which is mandatory for KCanG §16 documentation requirements.
3. **Multi-role seed accounts** — Admin, Staff, Member accounts cover the role hierarchy that regulatory audits require (who did what, with what authority).
4. **Deterministic UUIDs** — Critical for regulatory audit trail assertions: you can verify exactly which member received which distribution.
#### Findings
| # | Severity | Finding | Recommendation |
|---|----------|---------|----------------|
| D-1 | ⚠️ Warning | **Adult quota limit not tested.** Plan tests under-21 limit (30g/month) but does NOT test the adult limit (50g/month, max 25g/day). KCanG §3 Abs. 1 requires both. | Add test case: Attempt >25g single distribution for adult member → expect rejection |
| D-2 | ⚠️ Warning | **No THC% limit test for under-21.** KCanG §3 Abs. 2 Nr. 3 limits THC to 10% for under-21 members. Jonas Weber (under-21) is distributed "CBD Critical Mass" (5% THC) — but there's no test that tries to distribute a high-THC strain to an under-21 member. | Add negative test: Distribute "Amnesia Haze" (22% THC) to Jonas → expect rejection with specific error |
| D-3 | ️ Info | **Compliance deadlines seed uses fixed statuses.** The plan mentions PENDING, OVERDUE, COMPLETED but uses fixed dates. If tests rely on OVERDUE status, date drift will eventually make assertions wrong. | Use `NOW() - INTERVAL` for overdue deadlines, or recalculate status at assertion time |
| D-4 | ️ Info | **Distribution `recorded_by` references member UUID, not admin UUID.** In practice, distributions should be recorded by staff/admin, not self-service. The seed shows member `c1...001` as `recorded_by` which is technically allowed but non-standard for audits. | Consider using admin UUID `b1000000-...001` as `recorded_by` for realism |
| D-5 | ✅ Good | **Document retention testing.** Documents seed covers all categories (SATZUNG, PROTOKOLL, VERTRAG, SONSTIGES) — important for KCanG §19 documentation duties. |
| D-6 | ✅ Good | **Grow tracking covers lifecycle.** SEEDLING → VEGETATIVE → FLOWERING stages match KCanG §2 cultivation documentation requirements. |
#### Domain Verdict: ⚠️ ACCEPTABLE with D-1 and D-2 as recommended additions
The plan covers the core regulatory-critical paths (quota enforcement, audit trail, compliance deadlines, document management). The two missing negative tests (D-1 daily limit, D-2 THC% limit for under-21) are important regulatory edge cases but not plan-blocking — they can be added during implementation.
---
## 🔧 Expert 2: Architecture Expert (Next.js + Spring Boot + Playwright + Docker)
### Assessment: ⚠️ Concerns — 3 Issues (1 blocking)
#### Strengths
1. **Flyway repeatable migration for seed** (Option A) — Cleanest solution. `R__seed_test_data.sql` with profile-gated location is idiomatic Spring Boot + Flyway. Correct choice.
2. **API client abstraction** — Separating UI assertions from API-level verification is architecturally sound. Enables both black-box and white-box testing.
3. **Suite-level reset with ordered execution** — Given `fullyParallel: false` is already in the config, this is the pragmatic choice over per-test isolation.
4. **Existing Playwright project structure** — Plan correctly identifies adding an `integration` project with `dependencies: ["setup"]` — aligns with the existing 3-project pattern.
#### Findings
| # | Severity | Finding | Recommendation |
|---|----------|---------|----------------|
| A-1 | ❌ **Blocking** | **Seed timing contradiction.** Section 5.3 correctly identifies that `init.sql` as `/docker-entrypoint-initdb.d/99-seed.sql` runs at Postgres init — BEFORE Flyway creates tables (Flyway runs on backend startup). The recommended Option A (Flyway `R__seed_test_data.sql`) solves this, but Section 5.1 still shows `./scripts/seed/init.sql:/docker-entrypoint-initdb.d/99-seed.sql:ro` in the docker-compose override. These are contradictory — you can't do both. If you use Option A, the volume mount becomes dead code. If you keep the volume mount, Option A is unnecessary. | **Remove the `99-seed.sql` volume mount from `docker-compose.test.yml` Section 5.1.** The seed must come from Flyway `R__` migration only. The `scripts/seed/` files become the source-of-truth for copy into `src/main/resources/db/migration/test/`. |
| A-2 | ⚠️ Warning | **`tmpfs` for Postgres may cause issues with Docker Desktop on macOS.** Docker Desktop's Linux VM doesn't handle tmpfs the same as native Linux. On some versions, Postgres fails to start with tmpfs due to permission issues or the VM not forwarding tmpfs syscalls correctly. CI (GitHub Actions on `ubuntu-latest`) is fine, but local development on macOS may fail. | Add a conditional: use tmpfs only when `CI=true`, otherwise use regular volume. Or document this as "CI-only optimization" and keep the named volume for local test runs. |
| A-3 | ⚠️ Warning | **The `seed` container is still referenced in docker-compose.test.yml but plan says "Remove seed container."** Section 5.2 states "Remove `seed` container" as an improvement, but the existing file still has it. The plan must be explicit: in the new `docker-compose.test.yml`, the `seed` service is replaced by the Flyway `R__` migration + the global-setup DB readiness check. | Explicitly state: delete the `seed` service from docker-compose.test.yml. Replace the `depends_on: seed: condition: service_completed_successfully` on the `playwright` service with `depends_on: backend: condition: service_healthy`. |
| A-4 | ⚠️ Warning | **Playwright runs inside Docker container but mounts host `./cannamanage-frontend`.** The volume mount `./cannamanage-frontend:/app` means the container uses host `node_modules` (if present) or needs to install them. Since Playwright image doesn't include project deps, the test command (`npx playwright test`) will fail unless deps are installed first. Current `system-test.spec.ts` works because it's a single file with minimal deps, but 12 integration spec files with helpers will need the full `pnpm install` step. | Add `pnpm install --frozen-lockfile` before the test command in the playwright service, or use a multi-stage Dockerfile for the playwright service that pre-installs deps. |
| A-5 | ️ Info | **No explicit base URL override for API client.** The `ApiClient` connects to the backend directly (`baseUrl`). Inside Docker, this should be `http://backend:8080`, not `http://localhost:8080`. The plan shows `BASE_URL: http://frontend:3000` in the environment but doesn't define a `BACKEND_URL` for direct API calls. | Add `BACKEND_URL: http://backend:8080` to the playwright service environment. The API client should read from `process.env.BACKEND_URL ?? 'http://localhost:8080'`. |
| A-6 | ✅ Good | **Authentication reuse via storageState** — Correct pattern, avoids per-test login overhead. |
| A-7 | ✅ Good | **Profile-gated test endpoint** (`POST /api/v1/test/reset-db` only on `test` profile) — Proper security boundary. |
#### Architecture Verdict: ⚠️ REVISE — A-1 is contradictory and needs resolution
The seed timing contradiction (A-1) is a plan consistency error that will cause confusion during implementation. A-2 through A-5 are addressable during implementation without plan revision, but A-1 needs explicit resolution in the plan document.
---
## 🛡️ Expert 3: Risk & Reliability Expert (Test Reliability, CI Flakiness, Coverage)
### Assessment: ⚠️ Acceptable — manageable risks
#### Strengths
1. **"Never use `waitForTimeout`"** — Explicitly stated in Section 4.3. This is the #1 rule for non-flaky Playwright tests.
2. **Trace collection on first retry** — Correct debugging strategy for CI.
3. **Liberal timeouts for Docker networking** — 90s test / 60s navigation accounts for cold-start Docker overhead.
4. **DB verification via API** — Tests don't solely rely on UI state, which is inherently more fragile.
5. **Test ordering strategy** — Read-only tests first, CRUD tests last. Reduces state pollution.
#### Findings
| # | Severity | Finding | Recommendation |
|---|----------|---------|----------------|
| R-1 | ⚠️ Warning | **60+ tests in <5 minutes is ambitious.** Each test navigates, waits for API response, performs assertions. With Docker networking latency, expect 3-8 seconds per test. 60 tests × 5s average = 5 minutes. CRUD tests that submit forms and wait for toasts will be slower. Real expectation: 6-8 minutes. | Adjust success criterion to "< 8 minutes" or reduce test count per spec. Alternatively, enable parallel test execution for read-only specs (they don't mutate state). |
| R-2 | ⚠️ Warning | **Suite-level reset means CRUD test failures corrupt state for subsequent tests.** If test 3.2#3 (create member) fails mid-way (e.g., form submitted but toast timeout), the DB now has a partial member. All subsequent tests that count rows will fail with confusing "expected 5, got 6" errors. | Add a "CRUD section reset" mechanism: before each CRUD-heavy describe block, call `apiClient.resetDb()`. Or structure specs so each CRUD test verifies against its own created data, not against absolute counts. |
| R-3 | ⚠️ Warning | **No mention of `data-testid` strategy.** Section 9 lists this as an "open question" but it's actually critical for test reliability. CSS selectors and text-based selectors (`"5 Mitglieder"`) are brittle — a translation change, number format change, or design refactor breaks tests. | Decide NOW: use `data-testid` attributes on all interactive elements and key display elements. This is not optional for a 60+ test suite — it's a prerequisite. |
| R-4 | ⚠️ Warning | **Hardcoded expected values in tests.** Many assertions reference specific values: "5 Mitglieder", "Northern Lights 18.5% THC", "30€". If the seed data changes (even a typo fix), these tests break. | Create a `seed-constants.ts` file that exports expected values derived from a single source of truth. Tests import from this file. When seed changes, update one file. |
| R-5 | ️ Info | **No retry strategy for the DB reset endpoint.** If the `POST /api/v1/test/reset-db` call fails or times out (backend GC pause, connection pool exhaustion), the entire suite fails. | Add retry logic (3 attempts, 2s backoff) in the global-setup for the DB reset call. |
| R-6 | ️ Info | **Monthly quota seed uses fixed year/month (2024/12).** Tests checking quota display may show "no quota for current month" because the seed references December 2024, not the current month. | Use dynamic month in seed OR test assertions should navigate to the historical period, not assume "current month" view shows seed data. |
| R-7 | ️ Info | **No parallel execution plan for read-only tests.** The plan states `fullyParallel: false` globally, but read-only tests (all #1 and #2 cases per spec) could safely run in parallel since they don't mutate state. This would cut execution time by 30-40%. | Consider splitting into two Playwright projects: `integration-read` (parallel) and `integration-write` (serial). |
| R-8 | ✅ Good | **Artifact collection strategy is comprehensive** — screenshots, traces, backend logs, HTML report. This is sufficient for debugging CI failures. |
| R-9 | ✅ Good | **CI timeout of 10 minutes** — Appropriate safety margin above the expected 5-8 minute runtime. |
#### Reliability Verdict: ⚠️ ACCEPTABLE — R-2 and R-3 are the main risks
The plan will produce working tests, but without `data-testid` (R-3) and with suite-level-only reset (R-2), expect a 15-25% maintenance burden from flaky/cascading failures within the first 3 months. These are addressable during implementation without plan revision.
---
## Panel Synthesis
### Confidence Scores
| Expert | Confidence | Reasoning |
|--------|-----------|-----------|
| 🏛️ Domain | 82% | Core regulatory paths covered; 2 missing edge cases (daily limit, THC% limit for under-21) are non-blocking |
| 🔧 Architecture | 65% | Seed timing contradiction (A-1) is a consistency error that will cause implementation confusion |
| 🛡️ Reliability | 75% | Fundamentally sound approach but `data-testid` decision and reset granularity need resolution |
**Overall Panel Confidence: 74%**
### Combined Findings by Severity
#### ❌ Blocking (1)
| ID | Expert | Finding |
|----|--------|---------|
| A-1 | 🔧 Architecture | Seed timing contradiction: docker-compose.test.yml still mounts `init.sql` to `docker-entrypoint-initdb.d` while recommending Flyway `R__` migration. These are mutually exclusive approaches — plan must pick one and remove the other. |
#### ⚠️ Warnings (9)
| ID | Expert | Finding |
|----|--------|---------|
| D-1 | 🏛️ Domain | No adult daily quota limit (25g/day) test |
| D-2 | 🏛️ Domain | No THC% limit test for under-21 members |
| A-2 | 🔧 Architecture | tmpfs may fail on Docker Desktop macOS |
| A-3 | 🔧 Architecture | `seed` container removal not explicitly reflected in compose |
| A-4 | 🔧 Architecture | Playwright container needs `pnpm install` before tests |
| A-5 | 🔧 Architecture | No BACKEND_URL env for API client inside Docker |
| R-1 | 🛡️ Reliability | 5-minute target unrealistic for 60+ tests with Docker overhead |
| R-2 | 🛡️ Reliability | Suite-level reset causes cascading failures on CRUD test errors |
| R-3 | 🛡️ Reliability | `data-testid` strategy must be decided before implementation |
#### ️ Info (6)
| ID | Expert | Finding |
|----|--------|---------|
| D-3 | 🏛️ Domain | Fixed-date compliance deadlines may drift |
| D-4 | 🏛️ Domain | `recorded_by` should reference admin, not member |
| R-4 | 🛡️ Reliability | Hardcoded expected values — use seed-constants.ts |
| R-5 | 🛡️ Reliability | No retry on DB reset endpoint |
| R-6 | 🛡️ Reliability | Monthly quota seed uses fixed 2024/12, not current month |
| R-7 | 🛡️ Reliability | Read-only tests could run in parallel for speed |
---
## Panel Verdict
### 🔄 REVISE — 1 blocking finding must be resolved in the plan
**Required before implementation:**
1. Resolve A-1: Remove the `docker-entrypoint-initdb.d` volume mount from the proposed docker-compose.test.yml changes. Make it unambiguous that seed data flows through `R__seed_test_data.sql` via Flyway only.
**Strongly recommended (can be addressed during implementation):**
2. Resolve R-3: Commit to `data-testid` attributes as the selector strategy. Add a section "Selector Strategy" to the plan.
3. Add D-1 and D-2 test cases to the Distributions spec (regulatory completeness).
4. Add `pnpm install` to the playwright service command (A-4).
**Nice-to-have (implementation decisions):**
5. Adjust execution time target from 5 min to 8 min (R-1).
6. Add `BACKEND_URL` environment variable for API client (A-5).
7. Create `seed-constants.ts` for maintainable assertions (R-4).
---
*Panel review completed 2026-06-18 by Plan Reviewer mode.*
---
## v2 Re-Review (2026-06-18)
**Reviewer:** Roo (Plan Reviewer)
**Document reviewed:** `docs/sprint-12/cannamanage-sprint12-phase2-integration-tests.md` (v2)
**Purpose:** Verify all v1 findings have been properly addressed
---
### ❌ Blocker Resolution
| ID | v1 Finding | v2 Status | Evidence |
|----|-----------|-----------|----------|
| A-1 | Seed timing contradiction: docker-compose mounts `init.sql` to `docker-entrypoint-initdb.d` while recommending Flyway `R__` migration | ✅ **Resolved** | Section 1.3 explicitly declares "Decision (v2): Flyway-only seeding — NO Docker docker-entrypoint-initdb.d mount." Section 5.1 `docker-compose.test.yml` has NO volume mount on the `db` service, with comment confirming intent. The contradiction is eliminated. |
---
### ⚠️ Warning Resolution
| ID | Expert | v1 Finding | v2 Status | Evidence |
|----|--------|-----------|-----------|----------|
| D-1 | 🏛️ | No adult daily quota limit (25g/day) test | ✅ **Resolved** | New `13-kcang-regulatory.spec.ts` — test #1 (26g → rejection), test #6 (23g+3g exceeds daily), test #7 (23g+2g = exactly 25g → success). Thorough coverage of boundary. |
| D-2 | 🏛️ | No THC% limit test for under-21 members | ✅ **Resolved** | KCanG spec tests #3 (22% THC to U21 → rejection), #4 (0.5% THC to U21 → success), #5 (30g+ low-THC to U21 → quota error), #9 (UI shows "max. 10% THC" notice). Excellent coverage. |
| A-2 | 🔧 | tmpfs may fail on Docker Desktop macOS; network aliases unclear | ⚠️ **Partially** | Section 5.4 clearly documents Docker service-name networking (no custom aliases needed). However, `tmpfs` is still unconditional — no CI-only gating. Risk is LOW: CI runs on `ubuntu-latest` (native Linux), and local devs can override with `docker compose -f docker-compose.yml -f docker-compose.test.yml up`. Acceptable for implementation. |
| A-3 | 🔧 | `seed` container removal not explicitly reflected in compose | ✅ **Resolved** | Section 5.1 `docker-compose.test.yml` defines exactly 4 services: `db`, `backend`, `frontend`, `playwright`. No `seed` service exists. `playwright` depends on `backend: condition: service_healthy`. Clean. |
| A-4 | 🔧 | Playwright container needs `pnpm install` before tests | ✅ **Resolved** | Section 5.2 introduces `Dockerfile.playwright` with `pnpm install --frozen-lockfile` at build time. Dependencies are pre-installed in the image. |
| A-5 | 🔧 | No BACKEND_URL env for API client inside Docker | ✅ **Resolved** | Section 5.1 playwright service has `API_URL: http://backend:8080`. Section 2.7 global-setup uses this for health checks and API client initialization. |
| R-1 | 🛡️ | 5-minute target unrealistic for 60+ tests with Docker overhead | ⚠️ **Partially** | Plan now has per-test reset (~500ms × 70 = 35s overhead), making 5 min MORE ambitious. CI timeout is 10 min (appropriate). Test tagging splits `@smoke` (PR, <2 min) from `@full` (merge, 10 min). Section 7 success criteria still says "< 5 minutes total" — this is optimistic but the CI timeout and tagging strategy make it non-blocking. |
| R-2 | 🛡️ | Suite-level reset causes cascading failures on CRUD test errors | ✅ **Resolved** | Section 2.1 switches to per-test reset: "Decision (v2): Per-test reset via backend API endpoint + `beforeEach` hook." Each test calls `apiClient.resetDb()` — complete state isolation between tests. |
| R-3 | 🛡️ | `data-testid` strategy must be decided before implementation | ✅ **Resolved** | Section 2.2 commits to `data-testid` as mandatory. Naming convention defined (`<page>-<component>-<identifier>`), `selectors.ts` centralized file shown, implementation tracked as Phase 2C sub-task. |
---
### ️ Info Finding Resolution
| ID | Expert | v1 Finding | v2 Status | Evidence |
|----|--------|-----------|-----------|----------|
| D-3 | 🏛️ | Fixed-date compliance deadlines may drift | ✅ **Resolved** | Section 1.4 principle #3: "Use relative dates where possible (`NOW() - INTERVAL '7 days'`) for time-sensitive tests". Per-test reset re-runs seed each time, keeping relative dates fresh. |
| D-4 | 🏛️ | `recorded_by` should reference admin, not member | ️ Noted | Not explicitly addressed in v2 — minor realism improvement, can be done during seed implementation. |
| R-4 | 🛡️ | Hardcoded expected values — use seed-constants.ts | ⚠️ **Partially** | No explicit `seed-constants.ts` file created, but deterministic UUIDs (Section 1.4 #1) + per-test reset ensure assertions are stable. The `selectors.ts` centralizes locators but not data values. Acceptable — can be added during implementation when duplication becomes apparent. |
| R-5 | 🛡️ | No retry on DB reset endpoint | ️ Noted | No explicit retry logic shown for `resetDb()`. Implementation detail — the global-setup health check ensures backend is ready before any reset calls. Low risk. |
| R-6 | 🛡️ | Monthly quota seed uses fixed 2024/12, not current month | ✅ **Resolved** | Relative dates principle + Section 4.2 test tagging strategy (`@smoke`/`@full` with `--grep`). Seed data for quotas will use relative dates per principle #3. |
| R-7 | 🛡️ | Read-only tests could run in parallel for speed | ️ Noted | Not adopted in v2. Per-test reset makes parallelism less critical (each test is independent). Can be optimized later if execution time becomes a concern. |
---
### New Findings in v2
| ID | Severity | Finding | Impact |
|----|----------|---------|--------|
| v2-1 | ️ Info | **Volume mount + Dockerfile build overlap.** Section 5.1 mounts `./cannamanage-frontend/e2e:/app/e2e:ro` into the playwright container which also COPYs `e2e/` in `Dockerfile.playwright`. The volume mount overrides the built-in files at runtime. | This is an intentional Docker pattern (enables test iteration without rebuild) but should be documented to avoid confusion. Non-blocking. |
| v2-2 | ️ Info | **Success criterion "< 5 minutes total" vs per-test reset overhead.** 70+ tests × ~500ms reset + test execution (3-8s each) = realistic estimate is 6-8 minutes for `@full` suite. CI timeout of 10 min is correct, but the stated target is optimistic. | Cosmetic — CI timeout is appropriate. Adjust success criterion to "< 8 minutes" post-implementation based on actual measurements. |
| v2-3 | ️ Info | **`Dockerfile.playwright` pinned to `v1.49.0`** — this should match the Playwright version in `package.json` to avoid browser/API version mismatches. | Document: keep Dockerfile image version in sync with `@playwright/test` version in `package.json`. |
---
### Updated Confidence Scores
| Expert | v1 Confidence | v2 Confidence | Change | Reasoning |
|--------|--------------|--------------|--------|-----------|
| 🏛️ Domain | 82% | 95% | +13 | D-1 and D-2 fully resolved with 9 comprehensive KCanG test cases. Regulatory coverage is now excellent. |
| 🔧 Architecture | 65% | 92% | +27 | A-1 blocker completely eliminated. Dockerfile.playwright, API_URL, health checks — all addressed. Only tmpfs (A-2) remains partially open but low-risk. |
| 🛡️ Reliability | 75% | 90% | +15 | Per-test reset (R-2) and data-testid commitment (R-3) are the two highest-impact improvements. Execution time target (R-1) is slightly optimistic but non-blocking. |
**Overall Panel Confidence: 92%** (v1: 74%, Δ: +18)
---
### Panel Verdict (v2)
## ✅ APPROVED — Plan is ready for implementation
All blocking findings resolved. All critical warnings addressed. Remaining items (A-2 tmpfs conditional, R-1 time target, R-4 seed-constants.ts) are implementation-time decisions that don't require plan revision.
**Key improvements in v2:**
1. Seed timing contradiction eliminated — Flyway-only, no Docker init.sql
2. Per-test DB reset — complete test isolation via `beforeEach` + API endpoint
3. `data-testid` strategy committed — naming convention, selectors.ts, mandatory for Phase 2C
4. KCanG regulatory spec added — 9 comprehensive edge case tests covering §3 Abs. 1 + Abs. 2
5. Dedicated Playwright Dockerfile with pre-installed dependencies
6. Clear Docker networking documentation and health check strategy
**Recommendation:** Proceed to implementation. The 3 ️ info items (v2-1, v2-2, v2-3) can be addressed during Phase 2C/2D without plan revision.
---
*v2 Re-review completed 2026-06-18 by Plan Reviewer mode.*
---
## v3 Final Review (2026-06-18)
**Reviewer:** Roo (Plan Reviewer)
**Document reviewed:** `docs/sprint-12/cannamanage-sprint12-phase2-integration-tests.md` (v3 — final revision)
**Purpose:** Final gate review before implementation begins. All v2 partial/info items should now be fully resolved.
---
### 🏛️ Expert 1: Domain Expert (KCanG Regulatory Compliance)
#### Assessment: ✅ Excellent — No remaining gaps
| # | Check | Verdict | Evidence |
|---|-------|---------|----------|
| D-1 | Adult daily 25g limit tested | ✅ | KCanG spec tests #1, #6, #7 — boundary cases (26g reject, 23+3 reject, 23+2 pass) |
| D-2 | Under-21 THC% limit tested | ✅ | KCanG spec tests #3, #4, #5, #9 — Amnesia Haze 22% → reject, CBD Critical Mass 0.5% → pass, UI notice |
| D-3 | Compliance deadlines use relative dates | ✅ | Section 1.4 principle #3: `NOW() - INTERVAL` for time-sensitive tests, per-test reset keeps fresh |
| D-4 | `recorded_by` references admin UUID | ✅ | Section 1.2: explicitly states "v3: `recorded_by` = admin UUID `b1000000-...001`, not member UUID" |
| D-5 | Monthly quota limits differentiated (50g adult vs 30g U21) | ✅ | `seed-constants.ts` exports `KCANG.ADULT_MONTHLY_LIMIT_G: 50` and `UNDER21_MONTHLY_LIMIT_G: 30` |
| D-6 | Seed covers all regulatory-critical entity types | ✅ | Destruction records, compliance deadlines, distribution audit trail, grow lifecycle all seeded |
**New observation (v3):**
| # | Severity | Finding | Impact |
|---|----------|---------|--------|
| D-7 | ✅ Good | **`seed-constants.ts` exports KCanG limits as constants.** Tests import `KCANG.ADULT_DAILY_LIMIT_G` etc. — if regulations change (unlikely short-term, but possible with KCanG amendments), there is a single point of update. | Excellent maintainability for regulatory compliance. |
| D-8 | ️ Info | **No "combined monthly" test (adult at 50g boundary).** Tests cover 25g daily and under-21 30g monthly, but no test explicitly hits the adult 50g monthly ceiling. KCanG spec #2 tests 45g+6g=51g exceeding monthly, which implicitly covers it. | Covered via test #2 — no action needed. |
#### Domain Verdict: ✅ APPROVED — confidence 97%
All KCanG regulatory paths are comprehensively tested. The `seed-constants.ts` file makes regulatory limit changes trivial to propagate. The `recorded_by` fix ensures audit trail realism. No remaining domain gaps.
---
### 🔧 Expert 2: Architecture Expert (Next.js + Spring Boot + Playwright + Docker)
#### Assessment: ✅ Sound — All concerns resolved
| # | v2 Finding | v3 Resolution | Verdict |
|---|-----------|---------------|---------|
| A-1 | Seed timing contradiction | ✅ Eliminated in v2 (Flyway-only, confirmed in v3) | ✅ |
| A-2 | tmpfs macOS issues | ✅ `docker-compose.test.local.yml` override with named volume (Section 5.5) | ✅ |
| A-3 | Seed container removal | ✅ Only 4 services in compose: `db`, `backend`, `frontend`, `playwright` | ✅ |
| A-4 | Playwright needs pnpm install | ✅ `Dockerfile.playwright` with `pnpm install --frozen-lockfile` at build time | ✅ |
| A-5 | No BACKEND_URL for API client | ✅ `API_URL: http://backend:8080` in playwright service env | ✅ |
| v2-1 | Volume + Dockerfile overlap confusion | ✅ Explicit comment in Section 5.1 explaining the intentional pattern | ✅ |
| v2-3 | Playwright version pinning | ✅ Bold warning in Section 5.2: must match `@playwright/test` in package.json | ✅ |
**Architecture analysis of v3 additions:**
| # | Severity | Finding | Impact |
|---|----------|---------|--------|
| A-8 | ✅ Good | **`docker-compose.test.local.yml` is a clean override pattern.** Using compose file stacking (`-f base -f override`) is the Docker-native way to handle env-specific differences. No conditional logic in the base file — declarative and predictable. | Well-architected. |
| A-9 | ✅ Good | **`seed-constants.ts` placement at `cannamanage-frontend/e2e/seed-constants.ts`.** Correctly lives in the test scope, not in application code. Imported by specs via relative path. No runtime dependency. | Clean separation of concerns. |
| A-10 | ️ Info | **No automated enforcement of `seed-constants.ts` usage.** The "Rule: never hardcode seed values in specs" is documented but not lint-enforced. A custom ESLint rule (e.g., forbid UUID/number literals in spec files) could catch violations during PR review. | Low priority — PR review process is sufficient for a team of this size. Future improvement. |
| A-11 | ️ Info | **`R__seed_test_data.sql` checksum behavior.** Flyway repeatable migrations re-execute ONLY when the file checksum changes. If a developer adds seed data but forgets to update `seed-constants.ts`, tests will pass locally (fresh DB) but may confuse on next run. | Documented in Section 8 risks. Acceptable. |
#### Architecture Verdict: ✅ APPROVED — confidence 95%
All v1/v2 architectural concerns are fully resolved. The tmpfs override pattern is clean. The Dockerfile.playwright with version pinning is well-documented. Docker networking is clear. The volume mount explanation in v3 eliminates the last source of implementation confusion.
---
### 🛡️ Expert 3: Risk & Reliability Expert (Test Reliability, CI Flakiness, Maintenance)
#### Assessment: ✅ Solid — all high-risk items resolved
| # | v2 Finding | v3 Resolution | Verdict |
|---|-----------|---------------|---------|
| R-1 | 5-minute target unrealistic | ✅ Changed to "< 8 minutes for @full, < 2 minutes for @smoke" (Section 7) | ✅ |
| R-2 | Suite-level reset causes cascading failures | ✅ Resolved in v2 (per-test reset via beforeEach) | ✅ |
| R-3 | data-testid strategy undecided | ✅ Resolved in v2 (mandatory, naming convention, selectors.ts) | ✅ |
| R-4 | Hardcoded expected values | ✅ Complete `seed-constants.ts` with UUIDs, member data, KCanG limits, counts (Section 2.8) | ✅ |
| R-5 | No retry on DB reset endpoint | ️ Not explicitly added, but global-setup health check warms backend + pool before tests | Acceptable |
| R-6 | Fixed year/month in quota seed | ✅ Relative dates principle (Section 1.4 #3) + per-test reset re-runs seed | ✅ |
| R-7 | No parallel execution for read-only tests | ️ Not adopted — per-test reset makes it less critical | Acceptable |
**Reliability analysis of v3 additions:**
| # | Severity | Finding | Impact |
|---|----------|---------|--------|
| R-10 | ✅ Good | **`seed-constants.ts` is comprehensive.** Covers UUIDs, member metadata (`isUnder21`, `quotaUsedG`), strain THC%, KCanG limits, counts for every entity type. 80+ exported constants. | Single source of truth dramatically reduces maintenance burden from spec-level hardcoding. |
| R-11 | ✅ Good | **Time targets are now realistic.** 8-minute full suite with 10-minute CI timeout gives 25% safety margin. 2-minute smoke suite is achievable with ~15 tagged tests at 5-8s each. | Prevents false-failure CI red due to optimistic expectations. |
| R-12 | ✅ Good | **tmpfs override is opt-in, not opt-out.** Developers only use the local override if they experience issues. Default (tmpfs) works on CI and most macOS Docker Desktop versions. No "if CI then X else Y" conditional complexity. | Simple mental model for developers. |
| R-13 | ️ Info | **seed-constants.ts → R__seed_test_data.sql synchronization is manual.** When someone changes the SQL seed, they must also update the TypeScript constants file. There's no automated check that these stay in sync. | Acceptable risk for a small team. Could add a CI check later (parse SQL → generate constants → compare). |
**Flakiness risk assessment (final):**
| Risk Factor | v1 Score | v3 Score | Change |
|-------------|----------|----------|--------|
| State pollution between tests | 🔴 High | 🟢 Low | Per-test reset eliminates |
| Selector brittleness | 🔴 High | 🟢 Low | data-testid mandatory |
| Hardcoded assertion values | 🟡 Medium | 🟢 Low | seed-constants.ts |
| Time-dependent test data | 🟡 Medium | 🟢 Low | Relative dates in seed |
| Docker networking flakiness | 🟡 Medium | 🟢 Low | Health checks + liberal timeouts |
| CI execution time pressure | 🟡 Medium | 🟢 Low | Realistic 8-min target + 10-min timeout |
| macOS local dev issues | 🟡 Medium | 🟢 Low | docker-compose.test.local.yml override |
**Estimated maintenance burden:** < 5% of test development time (was 15-25% estimated in v1 review).
#### Reliability Verdict: ✅ APPROVED — confidence 94%
All high-risk flakiness vectors have been systematically addressed. The `seed-constants.ts` file is the most impactful v3 addition from a reliability perspective — it eliminates the "change seed, break 40 tests" failure mode. The realistic time targets prevent CI false-reds. The per-test reset (from v2) provides complete test isolation.
---
### Panel Synthesis (v3 Final)
#### Confidence Scores
| Expert | v1 | v2 | v3 | Reasoning |
|--------|----|----|----|-----------|
| 🏛️ Domain | 82% | 95% | **97%** | `recorded_by` fix + KCanG constants in `seed-constants.ts` close the last gaps |
| 🔧 Architecture | 65% | 92% | **95%** | tmpfs override + volume explanation + version pinning = all concerns fully resolved |
| 🛡️ Reliability | 75% | 90% | **94%** | `seed-constants.ts` + realistic time targets eliminate remaining medium-risk items |
**Overall Panel Confidence: 95%** (v1: 74% → v2: 92% → v3: 95%)
#### Remaining Items (all ️ Info — none blocking)
| ID | Expert | Finding | Priority |
|----|--------|---------|----------|
| A-10 | 🔧 | No lint enforcement of seed-constants usage | Future improvement |
| A-11 | 🔧 | SQL ↔ TS constants sync is manual | Acceptable for team size |
| R-5 | 🛡️ | No explicit retry on DB reset endpoint | Low risk, health check mitigates |
| R-13 | 🛡️ | seed-constants.ts sync with SQL is manual | Can add CI check post-implementation |
None of these require plan revision. All are implementation-time decisions or future improvements.
---
## Panel Verdict (v3 Final)
### ✅ APPROVED — Plan is complete, correct, and ready for implementation
**GO recommendation: Proceed immediately to Phase 2A.**
All 3 experts approve without blocking findings. The plan has matured through 3 iterations:
| Version | Verdict | Blockers | Confidence |
|---------|---------|----------|------------|
| v1 | 🔄 REVISE | 1 (seed timing) | 74% |
| v2 | ✅ APPROVED | 0 | 92% |
| v3 | ✅ APPROVED | 0 | 95% |
**v3 specifically resolved:**
1. ✅ tmpfs conditional — `docker-compose.test.local.yml` override (clean, declarative)
2. ✅ Realistic time targets — `@full` < 8 min, `@smoke` < 2 min (was "<5 min")
3.`seed-constants.ts` — 80+ exported constants, single source of truth for all test assertions
4.`recorded_by` fix — admin UUID for audit trail realism
5. ✅ Volume overlap documentation — intentional pattern explained inline
6. ✅ Version pinning — Playwright Docker image ↔ package.json sync rule with bold warning
**Quality assessment:** This is a production-grade integration test plan. The architecture (Flyway seed → per-test reset → data-testid selectors → seed-constants.ts) forms a cohesive, maintainable system. The phased implementation (2A→2E over 4 days) is realistic and correctly ordered.
**No further revision needed. Implementation can begin.**
---
*v3 Final panel review completed 2026-06-18 by Plan Reviewer mode.*
+377
View File
@@ -0,0 +1,377 @@
# Sprint 12 Implementation Plan: "Golden Test Standard"
**Datum:** 18.06.2026
**Autor:** Patrick Plate / Lumen (Planner)
**Status:** v1
**Basis:** cannamanage-sprint12-analysis.md
---
## Übersicht
| Phase | Beschreibung | Aufwand |
|-------|-------------|---------|
| Phase 1 | Documents Page — React Query + Wire Actions | ~2.5h |
| Phase 2 | Documents Page — UX Improvements | ~1h |
| Phase 3 | Board Page — Wire All Actions | ~1.5h |
| **Gesamt** | | **~5h** |
---
## Phase 1: Documents Page — React Query Integration + Action Wiring
### Step 1.1: Add React Query hooks to `services/documents.ts`
**File:** `cannamanage-frontend/src/services/documents.ts`
Add query hooks (pattern matches other services like `services/stock.ts`):
```typescript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
// Club ID constant (same pattern as info-board)
const CLUB_ID = "00000000-0000-0000-0000-000000000001"
export function useDocumentsQuery(category?: DocumentCategory) {
return useQuery({
queryKey: ["documents", category],
queryFn: () => listDocuments(CLUB_ID, category),
})
}
export function useUploadDocumentMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: {
title: string
category: DocumentCategory
accessLevel: DocumentAccessLevel
description: string | null
file: File
}) =>
uploadDocument(
CLUB_ID,
params.title,
params.category,
params.accessLevel,
params.description,
params.file
),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["documents"] })
},
})
}
export function useDeleteDocumentMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: string) => deleteDocument(id, CLUB_ID),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["documents"] })
},
})
}
```
### Step 1.2: Rewrite Documents Page with React Query
**File:** `cannamanage-frontend/src/app/(dashboard-layout)/documents/page.tsx`
Key changes:
1. Replace `useState(mockDocuments)` with `useDocumentsQuery()` + mock fallback
2. Wire upload form: collect form state → call `uploadMutation.mutate()`
3. Wire download button: `onClick={() => handleDownload(doc.id, doc.filename)}`
4. Wire delete button: confirmation dialog → `deleteMutation.mutate(doc.id)`
**Upload handler:**
```typescript
const uploadMutation = useUploadDocumentMutation()
function handleUpload() {
const fileInput = document.getElementById("file") as HTMLInputElement
const file = fileInput?.files?.[0]
if (!file || !title || !category) {
toast.error("Bitte alle Pflichtfelder ausfüllen")
return
}
uploadMutation.mutate(
{ title, category, accessLevel, description: description || null, file },
{
onSuccess: () => {
toast.success("Dokument hochgeladen")
setUploadOpen(false)
resetForm()
},
onError: () => toast.error("Upload fehlgeschlagen"),
}
)
}
```
**Download handler:**
```typescript
async function handleDownload(docId: string, filename: string) {
try {
const blob = await downloadDocument(docId)
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast.success("Download gestartet")
} catch {
toast.error("Download fehlgeschlagen")
}
}
```
**Delete handler with confirmation:**
```typescript
const deleteMutation = useDeleteDocumentMutation()
const [deleteTarget, setDeleteTarget] = useState<ClubDocument | null>(null)
// In AlertDialog:
function handleConfirmDelete() {
if (!deleteTarget) return
deleteMutation.mutate(deleteTarget.id, {
onSuccess: () => {
toast.success("Dokument gelöscht")
setDeleteTarget(null)
},
onError: () => toast.error("Löschen fehlgeschlagen"),
})
}
```
### Step 1.3: Add form state management to Upload Dialog
Add controlled state for all form fields:
```typescript
const [title, setTitle] = useState("")
const [category, setCategory] = useState<DocumentCategory | "">("")
const [accessLevel, setAccessLevel] = useState<DocumentAccessLevel>("ALL_MEMBERS")
const [description, setDescription] = useState("")
```
Wire each `<Input>` and `<Select>` to its state variable with `value` and `onChange`.
---
## Phase 2: Documents Page — UX Improvements
### Step 2.1: Category colors and icons
**File:** `cannamanage-frontend/src/app/(dashboard-layout)/documents/page.tsx`
Replace `getCategoryBadgeVariant()` with a richer mapping:
```typescript
const categoryStyles: Record<DocumentCategory, { color: string; icon: React.ReactNode }> = {
SATZUNG: {
color: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
icon: <BookOpen className="h-3 w-3" />,
},
PROTOKOLL: {
color: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400",
icon: <FileText className="h-3 w-3" />,
},
VERTRAG: {
color: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400",
icon: <FileSignature className="h-3 w-3" />,
},
VERSICHERUNG: {
color: "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400",
icon: <Shield className="h-3 w-3" />,
},
GENEHMIGUNG: {
color: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
icon: <CheckCircle className="h-3 w-3" />,
},
SONSTIGES: {
color: "bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400",
icon: <File className="h-3 w-3" />,
},
}
```
Use this in the Badge rendering:
```tsx
<Badge className={`inline-flex items-center gap-1 ${categoryStyles[category].color}`}>
{categoryStyles[category].icon}
{categoryLabels[category]}
</Badge>
```
### Step 2.2: Table column width constraints
Add column widths to prevent stretching:
```tsx
<TableHead className="w-[40%] min-w-[200px]">{t("name")}</TableHead>
<TableHead className="w-[120px]">{t("access")}</TableHead>
<TableHead className="w-[80px]">{t("size")}</TableHead>
<TableHead className="w-[100px]">{t("date")}</TableHead>
<TableHead className="w-[80px] text-right">{t("actions")}</TableHead>
```
Add `truncate` to the document title:
```tsx
<p className="font-medium truncate max-w-[250px]">{doc.title}</p>
```
### Step 2.3: Add upload loading state
Show a spinner/disabled state on the upload button while uploading:
```tsx
<Button
className="w-full"
onClick={handleUpload}
disabled={uploadMutation.isPending}
>
{uploadMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Upload className="mr-2 h-4 w-4" />
)}
{uploadMutation.isPending ? "Wird hochgeladen..." : t("uploadButton")}
</Button>
```
---
## Phase 3: Board Page — Wire All Actions
### Step 3.1: Add React Query hooks to `services/board.ts`
**File:** `cannamanage-frontend/src/services/board.ts`
Check existing service — add mutation hooks if not present:
```typescript
export function useCreatePositionMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: { title: string; description: string; sortOrder: number }) =>
createPosition(params),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["board"] }),
})
}
export function useElectBoardMemberMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: ElectBoardMemberRequest) => electBoardMember(params),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["board"] }),
})
}
export function useRemoveBoardMemberMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: string) => removeBoardMember(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["board"] }),
})
}
```
### Step 3.2: Wire "Position erstellen" dialog
**File:** `cannamanage-frontend/src/app/(dashboard-layout)/board/page.tsx`
1. Add form state for position fields
2. Replace close-only handler with mutation call:
```typescript
const createPosition = useCreatePositionMutation()
function handleCreatePosition() {
if (!posTitle.trim()) return
createPosition.mutate(
{ title: posTitle, description: posDesc, sortOrder: parseInt(sortOrder) || 0 },
{
onSuccess: () => {
toast.success("Position erstellt")
setPositionDialogOpen(false)
resetPositionForm()
},
onError: () => toast.error("Erstellen fehlgeschlagen"),
}
)
}
```
### Step 3.3: Wire "Wahl bestätigen" dialog
1. Add form state for election fields (positionId, memberId, electedAt, termStart, termEnd)
2. Replace close-only handler:
```typescript
const electMember = useElectBoardMemberMutation()
function handleElect() {
if (!selectedPosition || !selectedMember || !electedAt || !termStart) return
electMember.mutate(
{
positionId: selectedPosition,
memberId: selectedMember,
electedAt,
termStart,
termEnd: termEnd || undefined,
},
{
onSuccess: () => {
toast.success("Wahl bestätigt")
setElectDialogOpen(false)
},
onError: () => toast.error("Wahl fehlgeschlagen"),
}
)
}
```
### Step 3.4: Wire "Mitglied absetzen" button
Add confirmation dialog + handler:
```typescript
const removeMember = useRemoveBoardMemberMutation()
const [removeTarget, setRemoveTarget] = useState<string | null>(null)
// On the UserMinus button:
<Button variant="ghost" size="icon" onClick={() => setRemoveTarget(bm.id)}>
<UserMinus className="h-4 w-4" />
</Button>
// Confirmation AlertDialog:
function handleConfirmRemove() {
if (!removeTarget) return
removeMember.mutate(removeTarget, {
onSuccess: () => {
toast.success("Vorstandsmitglied abgesetzt")
setRemoveTarget(null)
},
onError: () => toast.error("Absetzen fehlgeschlagen"),
})
}
```
### Step 3.5: Replace mock data with React Query
Replace `mockBoardMembers` and `mockPositions` with:
```typescript
const { data: boardData, isLoading } = useBoardQuery()
const positions = boardData?.positions ?? mockPositions
const boardMembers = boardData?.members ?? mockBoardMembers
```
---
## Offene Fragen
- [ ] Soll die Documents-Upload-Funktion eine Fortschrittsanzeige haben (xhr.upload.onprogress), oder reicht ein einfacher Spinner?
- [ ] Board: Sollen die Member-Select-Optionen aus dem echten Members-Endpoint geladen werden, oder reichen hardcoded Options bis der Backend-Endpoint steht?
- [ ] Soll ein "Coming soon" Toast für den Reports-Center Tab hinzugefügt werden (momentan scheint es zu existieren als eigene Route)?
@@ -0,0 +1,305 @@
# Sprint 12 Test Plan: "Golden Test Standard"
**Datum:** 18.06.2026
**Autor:** Patrick Plate / Lumen (Planner)
**Status:** v1
**Basis:** cannamanage-sprint12-plan.md
---
## Testübersicht
| ID | Beschreibung | Typ | Seite | Status |
|----|-------------|-----|-------|--------|
| T-01 | Document upload end-to-end | E2E | documents | ⬜ |
| T-02 | Document download triggers file save | E2E | documents | ⬜ |
| T-03 | Document delete with confirmation | E2E | documents | ⬜ |
| T-04 | Document upload validation (missing fields) | E2E | documents | ⬜ |
| T-05 | Document category badges have distinct colors | Visual | documents | ⬜ |
| T-06 | Document table column widths don't stretch | Visual | documents | ⬜ |
| T-07 | Board: create position via dialog | E2E | board | ⬜ |
| T-08 | Board: elect member via dialog | E2E | board | ⬜ |
| T-09 | Board: remove member with confirmation | E2E | board | ⬜ |
| T-10 | All pages: no buttons without onClick handlers | Automated | all | ⬜ |
| T-11 | Documents page loads from API (React Query) | Integration | documents | ⬜ |
| T-12 | Board page loads from API (React Query) | Integration | board | ⬜ |
Status: ⬜ Offen | ✅ Bestanden | ❌ Fehlgeschlagen | ⏭️ Übersprungen
---
## Testfälle
### T-01: Document upload end-to-end
**Typ:** E2E (Playwright)
**Datei:** `e2e/sprint12-documents.spec.ts`
**Vorbedingungen:**
- Mock-Backend läuft oder Frontend-Mock-API aktiv
- Nutzer ist eingeloggt als Admin
**Szenarien:**
| # | Eingabe | Erwartetes Ergebnis |
|---|---------|-------------------|
| a | Upload-Button klicken → Dialog öffnet sich | Dialog mit Titel, Kategorie, Datei-Upload sichtbar |
| b | Alle Felder ausfüllen + Datei wählen → "Hochladen" klicken | Loading-Spinner erscheint, dann Success-Toast, Dialog schließt |
| c | Nach Upload: Dokumentenliste wird refresht | Neues Dokument erscheint in der Liste |
**Nachbedingungen:**
- Upload-Mutation wurde mit korrekten Parametern aufgerufen
- QueryClient hat `["documents"]` invalidiert
---
### T-02: Document download triggers file save
**Typ:** E2E (Playwright)
**Datei:** `e2e/sprint12-documents.spec.ts`
**Vorbedingungen:**
- Mindestens 1 Dokument in der Liste sichtbar
**Szenarien:**
| # | Eingabe | Erwartetes Ergebnis |
|---|---------|-------------------|
| a | Download-Icon-Button für ein Dokument klicken | Datei-Download wird ausgelöst (Playwright download event) |
| b | Backend nicht erreichbar → Download klicken | Error-Toast "Download fehlgeschlagen" |
**Nachbedingungen:**
- `downloadDocument(id)` wurde aufgerufen
- Blob wurde zu download-link konvertiert
---
### T-03: Document delete with confirmation
**Typ:** E2E (Playwright)
**Datei:** `e2e/sprint12-documents.spec.ts`
**Vorbedingungen:**
- Mindestens 1 Dokument in der Liste
**Szenarien:**
| # | Eingabe | Erwartetes Ergebnis |
|---|---------|-------------------|
| a | Delete-Icon klicken | Bestätigungs-Dialog erscheint ("Dokument wirklich löschen?") |
| b | "Abbrechen" im Dialog | Dialog schließt, Dokument bleibt |
| c | "Löschen" im Dialog bestätigen | Loading-State, dann Success-Toast, Dokument verschwindet aus Liste |
**Nachbedingungen:**
- `deleteDocument(id, clubId)` wurde aufgerufen
- Liste wurde refresht (query invalidation)
---
### T-04: Document upload validation (missing fields)
**Typ:** E2E (Playwright)
**Datei:** `e2e/sprint12-documents.spec.ts`
**Szenarien:**
| # | Eingabe | Erwartetes Ergebnis |
|---|---------|-------------------|
| a | Upload-Dialog öffnen → sofort "Hochladen" klicken (kein Titel, keine Datei) | Error-Toast "Bitte alle Pflichtfelder ausfüllen", kein API-Call |
| b | Titel eingeben aber keine Datei → "Hochladen" | Fehler-Hinweis |
| c | Datei wählen aber kein Titel → "Hochladen" | Fehler-Hinweis |
---
### T-05: Document category badges have distinct colors
**Typ:** Visual (Screenshot-Vergleich oder manuell)
**Datei:** `e2e/sprint12-documents.spec.ts` (Screenshot)
**Szenarien:**
| # | Kategorie | Erwartete Farbe | Icon vorhanden |
|---|-----------|----------------|----------------|
| a | SATZUNG | Blau (bg-blue-*) | ✅ BookOpen |
| b | PROTOKOLL | Lila (bg-purple-*) | ✅ FileText |
| c | VERTRAG | Amber (bg-amber-*) | ✅ FileSignature |
| d | VERSICHERUNG | Cyan (bg-cyan-*) | ✅ Shield |
| e | GENEHMIGUNG | Grün (bg-green-*) | ✅ CheckCircle |
| f | SONSTIGES | Grau (bg-gray-*) | ✅ File |
**Prüfmethode:** Screenshot machen, visuell prüfen dass alle 6 Kategorien klar unterscheidbar sind.
---
### T-06: Document table column widths don't stretch
**Typ:** Visual
**Datei:** `e2e/sprint12-documents.spec.ts` (Screenshot)
**Szenarien:**
| # | Viewport | Erwartetes Verhalten |
|---|----------|---------------------|
| a | Desktop (1280px) | Name-Spalte max 40% breit, truncated bei langen Titeln |
| b | Tablet (768px) | Table responsive, keine horizontale Scrollbar |
| c | Mobile (375px) | Graceful wrapping oder collapsed view |
---
### T-07: Board — create position via dialog
**Typ:** E2E (Playwright)
**Datei:** `e2e/sprint12-board.spec.ts`
**Szenarien:**
| # | Eingabe | Erwartetes Ergebnis |
|---|---------|-------------------|
| a | "Position hinzufügen" → Dialog öffnet | Formular mit Titel, Beschreibung, Reihenfolge |
| b | Titel "Beisitzer" + Beschreibung + Reihenfolge 6 → Speichern | Success-Toast, Dialog schließt, Position erscheint in Liste |
| c | Leerer Titel → Speichern | Kein API-Call, Validierung greift |
---
### T-08: Board — elect member via dialog
**Typ:** E2E (Playwright)
**Datei:** `e2e/sprint12-board.spec.ts`
**Szenarien:**
| # | Eingabe | Erwartetes Ergebnis |
|---|---------|-------------------|
| a | "Mitglied wählen" → Dialog öffnet | Formular mit Position-Select, Member-Select, Datum-Felder |
| b | Position + Mitglied + Wahldatum + Amtsbeginn ausfüllen → Bestätigen | Success-Toast, Dialog schließt, neues Board-Member in Karten |
| c | Keine Position gewählt → Bestätigen | Kein API-Call, Validierung |
---
### T-09: Board — remove member with confirmation
**Typ:** E2E (Playwright)
**Datei:** `e2e/sprint12-board.spec.ts`
**Szenarien:**
| # | Eingabe | Erwartetes Ergebnis |
|---|---------|-------------------|
| a | UserMinus-Icon auf Board-Member-Card klicken | Bestätigungs-Dialog "Wirklich absetzen?" |
| b | Bestätigen | Success-Toast, Member-Card verschwindet |
| c | Abbrechen | Dialog schließt, keine Änderung |
---
### T-10: All pages — no buttons without onClick handlers
**Typ:** Automated (Static analysis / Playwright audit)
**Datei:** `e2e/sprint12-button-audit.spec.ts`
**Ansatz:** Playwright-Test der jede Dashboard-Seite navigiert und prüft:
```typescript
const buttons = await page.locator('button').all()
for (const btn of buttons) {
// Verify button is either:
// 1. Inside a <Link> (navigation button)
// 2. Has an onClick or is type="submit" in a form
// 3. Is a dialog trigger (data-state attribute)
const isDisabled = await btn.getAttribute('disabled')
if (isDisabled) continue // disabled buttons are OK
// Click and verify something happens (no silent no-op)
}
```
**Seiten zu prüfen:**
- /documents
- /board
- /members
- /distributions
- /stock
- /grow
- /reports
- /calendar
- /forum
- /info-board
- /finance
- /assemblies
- /compliance
- /audit-log
- /settings/staff
---
### T-11: Documents page loads from API (React Query)
**Typ:** Integration
**Datei:** `e2e/sprint12-documents.spec.ts`
**Szenarien:**
| # | Bedingung | Erwartetes Ergebnis |
|---|-----------|-------------------|
| a | Backend erreichbar, Dokumente vorhanden | Dokumente aus API angezeigt (nicht Mock) |
| b | Backend nicht erreichbar | Mock-Fallback-Daten angezeigt (keine Fehlerseite) |
| c | Kategorie-Filter gewählt | Query-Key enthält Kategorie, neue Daten geladen |
---
### T-12: Board page loads from API (React Query)
**Typ:** Integration
**Datei:** `e2e/sprint12-board.spec.ts`
**Szenarien:**
| # | Bedingung | Erwartetes Ergebnis |
|---|-----------|-------------------|
| a | Backend erreichbar | Board-Mitglieder + Positionen aus API |
| b | Backend nicht erreichbar | Mock-Fallback angezeigt |
---
## Testdaten
### Documents
- Mock-Dokumente sind bereits in `documents/page.tsx` definiert (5 Dokumente, verschiedene Kategorien)
- Für Upload-Test: beliebige PDF-Datei < 5MB
- Für Download-Test: Mock-Backend muss Blob zurückgeben
### Board
- Mock-Board-Mitglieder sind bereits in `board/page.tsx` definiert (5 Positionen, 5 Mitglieder)
- Für Election-Test: Member-IDs aus Mock-Daten verwenden
---
## Testabdeckung
| Komponente | E2E | Integration | Visual | Gesamt |
|-----------|-----|-------------|--------|--------|
| Documents (upload) | 2 | 1 | 0 | 3 |
| Documents (download) | 1 | 0 | 0 | 1 |
| Documents (delete) | 1 | 0 | 0 | 1 |
| Documents (UX) | 0 | 0 | 2 | 2 |
| Board (create pos) | 1 | 1 | 0 | 2 |
| Board (elect) | 1 | 0 | 0 | 1 |
| Board (remove) | 1 | 0 | 0 | 1 |
| All pages audit | 1 | 0 | 0 | 1 |
| **Summe** | **8** | **2** | **2** | **12** |
---
## Playwright Test File Structure
```
e2e/
├── sprint12-documents.spec.ts # T-01 through T-06, T-11
├── sprint12-board.spec.ts # T-07 through T-09, T-12
└── sprint12-button-audit.spec.ts # T-10
```
## Ausführung
```bash
cd cannamanage-frontend
npx playwright test e2e/sprint12-*.spec.ts
```