add playwright e2e tests for onboarding, contact tracker

- install playwright, configure for chromium with vite dev server
- add onboarding flow test: redirect, form fill, navigation to prozess
- add contact tracker test: create contact, verify in list
- fix onboarding form navigating to /onboarding instead of /prozess
- fix date rendering crash in contact list (cast datum to text)
- exclude @electric-sql/pglite from vite optimizeDeps (WASM bundle)
- add test-results/, playwright-report/ to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 11:42:00 +01:00
parent e398d53315
commit c31948b997
9 changed files with 79 additions and 2 deletions

4
.gitignore vendored
View File

@@ -11,6 +11,10 @@ dist/
# Mise local overrides # Mise local overrides
.mise.local.toml .mise.local.toml
# Playwright
test-results/
playwright-report/
# Logs # Logs
*.log *.log

View File

@@ -25,6 +25,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.6", "@biomejs/biome": "^2.4.6",
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@types/node": "^25.4.0", "@types/node": "^25.4.0",
@@ -352,6 +353,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
@@ -1112,6 +1115,10 @@
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
@@ -1480,6 +1487,8 @@
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],

29
e2e/kontakte.spec.ts Normal file
View File

@@ -0,0 +1,29 @@
import { expect, test } from "@playwright/test";
test.beforeEach(async ({ page }) => {
await page.goto("/onboarding");
// Wait for PGlite to initialize and the form to render
await page.getByLabel("Name").waitFor({ timeout: 15000 });
await page.getByLabel("Name").fill("Test User");
await page.getByLabel("PLZ").fill("10115");
await page.getByLabel("Ort").fill("Berlin");
await page.getByLabel("Krankenkasse").fill("TK");
await page.getByRole("button", { name: "Weiter" }).click();
await expect(page).toHaveURL(/prozess/, { timeout: 10000 });
});
test("add a new contact and see it in the list", async ({ page }) => {
await page.getByRole("link", { name: "Kontakte" }).click();
await expect(page).toHaveURL(/kontakte/);
await page.getByRole("link", { name: /Neu/ }).click();
await expect(page).toHaveURL(/kontakte\/neu/);
await page.getByLabel("Name", { exact: false }).first().fill("Dr. Schmidt");
await page.getByLabel("Stadt").fill("Berlin");
await page.getByRole("button", { name: "Speichern" }).click();
await expect(page).toHaveURL(/\/kontakte$/, { timeout: 10000 });
await expect(page.getByText("Dr. Schmidt")).toBeVisible({ timeout: 10000 });
});

18
e2e/onboarding.spec.ts Normal file
View File

@@ -0,0 +1,18 @@
import { expect, test } from "@playwright/test";
test("onboarding flow redirects new user and saves profile", async ({
page,
}) => {
await page.goto("/");
// PGlite WASM initialization takes time; wait for the redirect to complete
await expect(page).toHaveURL(/onboarding/, { timeout: 15000 });
await page.getByLabel("Name").fill("Max Mustermann");
await page.getByLabel("PLZ").fill("10115");
await page.getByLabel("Ort").fill("Berlin");
await page.getByLabel("Krankenkasse").fill("TK");
await page.getByRole("button", { name: "Weiter" }).click();
await expect(page).toHaveURL(/prozess/, { timeout: 10000 });
await expect(page.getByText("Dein Fortschritt")).toBeVisible();
});

View File

@@ -33,6 +33,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.6", "@biomejs/biome": "^2.4.6",
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@types/node": "^25.4.0", "@types/node": "^25.4.0",

13
playwright.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "e2e",
webServer: {
command: "bun dev",
port: 5173,
reuseExistingServer: true,
},
use: {
baseURL: "http://localhost:5173",
},
});

View File

@@ -25,7 +25,7 @@ export function useTherapeutenListe() {
return useDbQuery<TherapeutMitKontakte>(` return useDbQuery<TherapeutMitKontakte>(`
SELECT SELECT
t.id, t.name, t.stadt, t.therapieform, t.id, t.name, t.stadt, t.therapieform,
(SELECT k.datum FROM kontakt k WHERE k.therapeut_id = t.id ORDER BY k.datum DESC LIMIT 1) as letzter_kontakt, (SELECT k.datum::text FROM kontakt k WHERE k.therapeut_id = t.id ORDER BY k.datum DESC LIMIT 1) as letzter_kontakt,
(SELECT k.ergebnis FROM kontakt k WHERE k.therapeut_id = t.id ORDER BY k.datum DESC LIMIT 1) as letztes_ergebnis, (SELECT k.ergebnis FROM kontakt k WHERE k.therapeut_id = t.id ORDER BY k.datum DESC LIMIT 1) as letztes_ergebnis,
(SELECT COUNT(*) FROM kontakt k WHERE k.therapeut_id = t.id) as kontakte_gesamt (SELECT COUNT(*) FROM kontakt k WHERE k.therapeut_id = t.id) as kontakte_gesamt
FROM therapeut t FROM therapeut t

View File

@@ -30,7 +30,7 @@ export function OnboardingForm() {
value.aktueller_schritt, value.aktueller_schritt,
], ],
); );
navigate({ to: "/onboarding" }); navigate({ to: "/prozess" });
}, },
}); });

View File

@@ -19,6 +19,9 @@ export default defineConfig({
manifest: false, manifest: false,
}), }),
], ],
optimizeDeps: {
exclude: ["@electric-sql/pglite"],
},
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),