feat(sprint-5): Phase 7 — System test harness

- docker-compose.test.yml: full stack test profile with seed + playwright
- scripts/seed/init.sql: test data (admin, members, batches, distributions)
- scripts/seed/seed.sh: backend readiness validation script
- e2e/system-test.spec.ts: full user journey against real/mock stack
- package.json: test:e2e, test:system, test:all scripts
- scripts/README.md: system test documentation and usage instructions
This commit is contained in:
Patrick Plate
2026-06-12 20:39:09 +02:00
parent 2cc8c89944
commit d1487539b6
6 changed files with 497 additions and 1 deletions
@@ -0,0 +1,158 @@
import { expect, test } from "@playwright/test"
import type { Page } from "@playwright/test"
/**
* System Integration Test — runs against the REAL Docker stack or mock backend.
*
* Environment:
* - BASE_URL: set by docker-compose.test.yml (http://frontend:3000)
* - Falls back to http://localhost:3000 for local dev with mock backend
*
* Test data (seeded via scripts/seed/init.sql):
* - Admin: admin@test.de / test123
* - 5 members, 3 batches, 3 distributions
*/
const BASE = process.env.BASE_URL || "http://localhost:3000"
test.describe("System Integration Test", () => {
test.describe.configure({ mode: "serial" })
let page: Page
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
})
test.afterAll(async () => {
await page.close()
})
test("login page loads correctly", async () => {
await page.goto(`${BASE}/login`)
await page.waitForLoadState("networkidle")
// Login page should have email and password fields
const emailInput = page.locator('input[name="email"], input[type="email"]')
const passwordInput = page.locator(
'input[name="password"], input[type="password"]'
)
await expect(emailInput).toBeVisible()
await expect(passwordInput).toBeVisible()
// Should have a submit button
const submitButton = page.locator('button[type="submit"]')
await expect(submitButton).toBeVisible()
})
test("admin can log in with seeded credentials", async () => {
await page.goto(`${BASE}/login`)
await page.waitForLoadState("networkidle")
// Fill login form
await page.fill('input[name="email"], input[type="email"]', "admin@test.de")
await page.fill('input[name="password"], input[type="password"]', "test123")
await page.click('button[type="submit"]')
// Wait for navigation — should redirect to dashboard
await page.waitForURL(/dashboard|\//, { timeout: 15000 })
// Verify we're on an authenticated page (not still on login)
const url = page.url()
expect(url).not.toContain("/login")
})
test("dashboard displays content after login", async () => {
// Navigate to dashboard explicitly
await page.goto(`${BASE}/dashboard`)
await page.waitForLoadState("networkidle")
// Dashboard should have recognizable content
const heading = page.locator("h1, h2, h3").first()
await expect(heading).toBeVisible({ timeout: 10000 })
// Should not be on login page (redirected back)
const url = page.url()
expect(url).not.toContain("/login")
})
test("members page shows member data", async () => {
await page.goto(`${BASE}/members`)
await page.waitForLoadState("networkidle")
// Should have a table or list of members
const content = page.locator(
'table, [role="table"], [data-testid="members-list"]'
)
await expect(content).toBeVisible({ timeout: 10000 })
})
test("distributions page is accessible", async () => {
await page.goto(`${BASE}/distributions`)
await page.waitForLoadState("networkidle")
// Should have distribution content
const content = page.locator(
'table, [role="table"], [data-testid="distributions-list"]'
)
await expect(content).toBeVisible({ timeout: 10000 })
})
test("stock page is accessible", async () => {
await page.goto(`${BASE}/stock`)
await page.waitForLoadState("networkidle")
// Should have stock/batch content
const content = page.locator(
'table, [role="table"], [data-testid="stock-list"]'
)
await expect(content).toBeVisible({ timeout: 10000 })
})
test("reports page is accessible", async () => {
await page.goto(`${BASE}/reports`)
await page.waitForLoadState("networkidle")
// Reports page should mention "Monatsbericht" or report types
const reportContent = page.locator("text=Monatsbericht, text=Report")
await expect(reportContent.first()).toBeVisible({ timeout: 10000 })
})
test("navigation sidebar works", async () => {
await page.goto(`${BASE}/dashboard`)
await page.waitForLoadState("networkidle")
// Check that main navigation links exist
const navLinks = page.locator("nav a, aside a")
const count = await navLinks.count()
expect(count).toBeGreaterThan(0)
})
test("no console errors on critical pages", async () => {
const errors: string[] = []
page.on("console", (msg) => {
if (msg.type() === "error") {
errors.push(msg.text())
}
})
// Visit each critical page
const criticalPages = ["/dashboard", "/members", "/distributions", "/stock"]
for (const path of criticalPages) {
await page.goto(`${BASE}${path}`)
await page.waitForLoadState("networkidle")
}
// Filter out known non-critical errors (e.g., favicon, source maps)
const criticalErrors = errors.filter(
(e) =>
!e.includes("favicon") &&
!e.includes(".map") &&
!e.includes("hydration")
)
expect(criticalErrors).toHaveLength(0)
})
})
+4 -1
View File
@@ -13,7 +13,10 @@
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"format": "prettier --ignore-path .gitignore --write ."
"format": "prettier --ignore-path .gitignore --write .",
"test:e2e": "playwright test e2e/full-check.spec.ts e2e/functional-flows.spec.ts",
"test:system": "playwright test e2e/system-test.spec.ts",
"test:all": "playwright test"
},
"engines": {
"node": ">=22",