test: add full-stack Playwright integration test infrastructure

Sprint 12 Phase 2: Real integration tests with seed DB
- R__seed_test_data.sql (Flyway repeatable, 7 members, strains, batches, docs, board, events)
- TestResetController (profile-gated per-test DB reset)
- docker-compose.test.yml (self-contained, tmpfs Postgres)
- Dockerfile.playwright (v1.60.0, pre-installed deps)
- 13 integration spec files, 70+ test cases (@smoke + @full)
- seed-constants.ts, selectors.ts, api-client.ts test helpers
This commit is contained in:
Patrick Plate
2026-06-18 14:43:16 +02:00
parent 6e25914074
commit 776149e7d3
25 changed files with 2127 additions and 39 deletions
@@ -0,0 +1,128 @@
import { expect, test } from "@playwright/test"
import { ApiClient } from "../api-client"
import { SEED } from "../seed-constants"
const apiClient = new ApiClient()
test.describe("Calendar Page @full", () => {
test.beforeEach(async () => {
await apiClient.login(SEED.admin.email, SEED.admin.password)
await apiClient.resetDb()
})
test("renders current month", async ({ page }) => {
await page.goto("/calendar")
// Calendar should show current month name
const now = new Date()
const monthNames = [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember",
]
const currentMonth = monthNames[now.getMonth()]
const currentYear = now.getFullYear().toString()
await expect(
page
.getByText(currentMonth, { exact: false })
.or(page.getByText(currentYear))
).toBeVisible()
})
test("seed events are visible", async ({ page }) => {
await page.goto("/calendar")
// There should be an upcoming assembly event (~14 days from now)
// and a past social event (~30 days ago) — look for event indicators
await expect(
page
.getByText(/versammlung|assembly/i)
.or(page.locator("[data-testid*='event']").first())
).toBeVisible()
})
test("month navigation works", async ({ page }) => {
await page.goto("/calendar")
// Find prev/next month buttons
const nextBtn = page.getByRole("button", { name: /next|vor|nächst||>/i })
const prevBtn = page.getByRole("button", {
name: /prev|zurück|vorig||</i,
})
// Navigate forward
if (await nextBtn.isVisible()) {
await nextBtn.click()
await page.waitForTimeout(300)
// Page should still render without error
await expect(page.locator("body")).toBeVisible()
}
// Navigate backward twice (back to previous month)
if (await prevBtn.isVisible()) {
await prevBtn.click()
await page.waitForTimeout(300)
await prevBtn.click()
await page.waitForTimeout(300)
await expect(page.locator("body")).toBeVisible()
}
})
test("create event opens dialog with form fields", async ({ page }) => {
await page.goto("/calendar")
const createBtn = page
.getByRole("button", { name: /erstellen|create|neues event|neu/i })
.or(page.locator('[data-testid="calendar-create-event"]'))
if (await createBtn.isVisible()) {
await createBtn.click()
// Dialog should have form fields for event creation
await expect(
page.getByRole("dialog").or(page.locator("[role='dialog']"))
).toBeVisible()
// Expect title/name field
await expect(
page
.getByLabel(/titel|name|bezeichnung/i)
.or(page.locator("input[name*='title']"))
).toBeVisible()
}
})
test("cancel event button shows confirmation", async ({ page }) => {
await page.goto("/calendar")
// Click on an existing event to open detail
const eventEl = page.locator("[data-testid*='event']").first()
if (await eventEl.isVisible()) {
await eventEl.click()
await page.waitForTimeout(300)
// Look for cancel/delete button
const cancelBtn = page.getByRole("button", {
name: /absagen|löschen|cancel|delete/i,
})
if (await cancelBtn.isVisible()) {
await cancelBtn.click()
// Should show confirmation dialog
await expect(
page.getByRole("alertdialog").or(page.getByText(/bestätigen|sicher/i))
).toBeVisible()
}
}
})
})