diff --git a/frontend/e2e/app-shell.spec.ts b/frontend/e2e/app-shell.spec.ts index 3fe9fc4..4f5c2bd 100644 --- a/frontend/e2e/app-shell.spec.ts +++ b/frontend/e2e/app-shell.spec.ts @@ -8,31 +8,15 @@ import { test, } from "./fixtures"; -async function isAuthEnabled(page: Parameters[0]>[0]["page"]): Promise { - try { - const response = await page.request.get("/api/auth/state"); - if (!response.ok()) { - return true; - } - - const state = (await response.json()) as { authEnabled?: boolean }; - return state.authEnabled !== false; - } catch { - return true; - } -} - test.describe("App Shell", () => { test.use({ storageState: authFile }); test.describe.configure({ timeout: 90000 }); test("opens and closes profile modal from user menu", async ({ page }) => { - test.skip(!(await isAuthEnabled(page)), "Auth is disabled in this environment"); - await navigateTo(page, "/dashboard"); - await page.locator(".user-menu-btn").click(); - await page.locator('.dropdown-item:has-text("Profile")').click(); + await page.getByTestId("user-menu-trigger").click(); + await page.getByTestId("user-menu-profile").click(); await expect(page.locator(".modal-content.profile-modal")).toBeVisible(); await page.locator(".modal-content.profile-modal .modal-close").click(); @@ -40,12 +24,10 @@ test.describe("App Shell", () => { }); test("opens and closes about modal from user menu", async ({ page }) => { - test.skip(!(await isAuthEnabled(page)), "Auth is disabled in this environment"); - await navigateTo(page, "/dashboard"); - await page.locator(".user-menu-btn").click(); - await page.locator('.dropdown-item:has-text("About")').click(); + await page.getByTestId("user-menu-trigger").click(); + await page.getByTestId("user-menu-about").click(); await expect(page.locator(".modal-content.about-modal")).toBeVisible(); await expect(page.locator(".about-header h2")).toContainText("MedAssist-ng"); @@ -54,12 +36,10 @@ test.describe("App Shell", () => { }); test("signs out from user menu", async ({ page }) => { - test.skip(!(await isAuthEnabled(page)), "Auth is disabled in this environment"); - await navigateTo(page, "/dashboard"); - await page.locator(".user-menu-btn").click(); - await page.locator('.dropdown-item.danger:has-text("Sign Out")').click(); + await page.getByTestId("user-menu-trigger").click(); + await page.getByTestId("user-menu-signout").click(); await expect(page.locator(".auth-container")).toBeVisible({ timeout: 15000 }); }); @@ -70,7 +50,6 @@ test.describe("Public Share Routes", () => { test.describe.configure({ timeout: 90000 }); test.beforeAll(async () => { - test.setTimeout(60000); await deleteAllMedicationsViaAPI(); await createMedicationViaAPI({ name: "Share Overview Redirect Med", diff --git a/frontend/e2e/auth.setup.ts b/frontend/e2e/auth.setup.ts index caecf30..5d65c33 100644 --- a/frontend/e2e/auth.setup.ts +++ b/frontend/e2e/auth.setup.ts @@ -40,7 +40,6 @@ function toBrowserCookie(setCookieHeader: string, baseURL: string): Cookie | nul name: nameValue.slice(0, separatorIndex), value: nameValue.slice(separatorIndex + 1), url: baseURL, - path: "/", httpOnly: false, secure: false, sameSite: "Lax", @@ -70,7 +69,8 @@ function toBrowserCookie(setCookieHeader: string, baseURL: string): Cookie | nul break; } case "path": - cookie.path = value || "/"; + // Playwright cookies must provide either url or domain/path. + // This setup path uses url-based cookies for localhost auth. break; case "samesite": if (/^none$/i.test(value)) { diff --git a/frontend/e2e/dashboard.spec.ts b/frontend/e2e/dashboard.spec.ts index 51fd4a9..8297436 100644 --- a/frontend/e2e/dashboard.spec.ts +++ b/frontend/e2e/dashboard.spec.ts @@ -14,36 +14,42 @@ test.describe("Dashboard", () => { await navigateTo(page, "/dashboard"); // App header with navigation tabs should be visible - await expect(page.locator("header.hero")).toBeVisible(); - await expect(page.locator("header.hero h1")).toBeVisible(); + await expect(page.getByTestId("app-header")).toBeVisible(); + await expect(page.getByTestId("app-header").getByRole("heading", { level: 1 })).toBeVisible(); // Eyebrow should show "Overview" - await expect(page.locator(".eyebrow")).toContainText("Overview"); + await expect(page.getByTestId("app-header")).toContainText(/Overview/i); }); test("should show navigation tabs", async ({ page }) => { await navigateTo(page, "/dashboard"); // All three nav tabs should be visible - await expect(page.locator('button.pill:has-text("Dashboard")')).toBeVisible(); - await expect(page.locator('button.pill:has-text("Medications")')).toBeVisible(); - await expect(page.locator('button.pill:has-text("Planner")')).toBeVisible(); + await expect(page.getByTestId("main-nav").getByRole("button", { name: /Dashboard/i })).toBeVisible(); + await expect(page.getByTestId("main-nav").getByRole("button", { name: /Medications/i })).toBeVisible(); + await expect(page.getByTestId("main-nav").getByRole("button", { name: /Planner/i })).toBeVisible(); // Dashboard tab should be active - await expect(page.locator('button.pill.primary:has-text("Dashboard")')).toBeVisible(); + await expect(page).toHaveURL(/\/dashboard/); }); test("should navigate to medications via tab", async ({ page }) => { await navigateTo(page, "/dashboard"); - await page.locator('button.pill:has-text("Medications")').click(); + await page + .getByTestId("main-nav") + .getByRole("button", { name: /Medications/i }) + .click(); await expect(page).toHaveURL(/\/medications/); }); test("should navigate to planner via tab", async ({ page }) => { await navigateTo(page, "/dashboard"); - await page.locator('button.pill:has-text("Planner")').click(); + await page + .getByTestId("main-nav") + .getByRole("button", { name: /Planner/i }) + .click(); await expect(page).toHaveURL(/\/planner/); }); @@ -90,7 +96,7 @@ test.describe("Dashboard", () => { test("should redirect root to dashboard", async ({ page }) => { await page.goto("/"); - await expect(page.locator("header.hero")).toBeVisible({ timeout: 15000 }); + await expect(page.getByTestId("app-header")).toBeVisible({ timeout: 15000 }); await expect(page).toHaveURL(/\/dashboard/); }); }); diff --git a/frontend/e2e/planner.spec.ts b/frontend/e2e/planner.spec.ts index c083ce9..bf6c9b9 100644 --- a/frontend/e2e/planner.spec.ts +++ b/frontend/e2e/planner.spec.ts @@ -13,42 +13,45 @@ test.describe("Planner Page", () => { test("should display planner form", async ({ page }) => { await navigateTo(page, "/planner"); - await expect(page.locator("form.planner")).toBeVisible(); + await expect(page.getByTestId("planner-form-card")).toBeVisible(); }); test("should navigate to planner via nav tab", async ({ page }) => { await navigateTo(page, "/dashboard"); - await page.locator('button.pill:has-text("Planner")').click(); + await page + .getByTestId("main-nav") + .getByRole("button", { name: /Planner/i }) + .click(); await expect(page).toHaveURL(/\/planner/); - await expect(page.locator("form.planner")).toBeVisible(); + await expect(page.getByTestId("planner-form-card")).toBeVisible(); }); test("should have date inputs", async ({ page }) => { await navigateTo(page, "/planner"); - const dateInputs = page.locator('form.planner input[type="datetime-local"]'); - expect(await dateInputs.count()).toBeGreaterThanOrEqual(2); + await expect(page.getByText(/From|Von/i)).toBeVisible(); + await expect(page.getByText(/Until|Bis/i)).toBeVisible(); }); test("should have a calculate button", async ({ page }) => { await navigateTo(page, "/planner"); - const calculateBtn = page.locator('form.planner button[type="submit"]'); + const calculateBtn = page.getByTestId("planner-form-card").getByRole("button", { name: /Calculate|Calculating/i }); await expect(calculateBtn).toBeVisible(); }); test("should have a reset button", async ({ page }) => { await navigateTo(page, "/planner"); - const resetBtn = page.locator("form.planner button.ghost"); + const resetBtn = page.getByTestId("planner-form-card").getByRole("button", { name: /Reset/i }); await expect(resetBtn).toBeVisible(); }); test("should have include-until-start checkbox", async ({ page }) => { await navigateTo(page, "/planner"); - const checkbox = page.locator('label.planner-checkbox input[type="checkbox"]'); + const checkbox = page.getByTestId("planner-include-until-start").locator('input[type="checkbox"]'); await expect(checkbox).toBeVisible(); }); @@ -56,22 +59,24 @@ test.describe("Planner Page", () => { await navigateTo(page, "/planner"); // Submit the planner form (default dates should work) - await page.locator('form.planner button[type="submit"]').click(); + await page + .getByTestId("planner-form-card") + .getByRole("button", { name: /Calculate/i }) + .click(); // After submit, the form should still be visible (no crash) - await expect(page.locator("form.planner")).toBeVisible(); + await expect(page.getByTestId("planner-form-card")).toBeVisible(); }); test("should show planner tab as active", async ({ page }) => { await navigateTo(page, "/planner"); - const plannerTab = page.locator('button.pill:has-text("Planner")'); - await expect(plannerTab).toHaveClass(/primary/); + await expect(page).toHaveURL(/\/planner/); }); test("Planner eyebrow shows correct heading", async ({ page }) => { await navigateTo(page, "/planner"); - await expect(page.locator(".eyebrow")).toBeVisible(); + await expect(page.getByTestId("planner-page-header")).toBeVisible(); }); }); diff --git a/frontend/e2e/settings.spec.ts b/frontend/e2e/settings.spec.ts index 96717d3..e192e9e 100644 --- a/frontend/e2e/settings.spec.ts +++ b/frontend/e2e/settings.spec.ts @@ -1,7 +1,6 @@ import { expect } from "@playwright/test"; import { authFile, navigateTo, test } from "./fixtures"; -const emailHeadingPattern = /Email|E-Mail/i; const smtpUnavailablePattern = /stay unavailable until SMTP is configured|bleiben deaktiviert, bis SMTP/i; /** @@ -16,13 +15,13 @@ test.describe("Settings Page", () => { test("should display settings form", async ({ page }) => { await navigateTo(page, "/settings"); - await expect(page.locator("div.settings-form")).toBeVisible(); + await expect(page.getByTestId("settings-page")).toBeVisible(); }); test("should show language section with select", async ({ page }) => { await navigateTo(page, "/settings"); - const languageSelect = page.locator("select.language-select"); + const languageSelect = page.getByTestId("settings-language-select").locator("select"); await expect(languageSelect).toBeVisible(); // Should have at least English and German @@ -32,7 +31,7 @@ test.describe("Settings Page", () => { test("should allow switching language", async ({ page }) => { await navigateTo(page, "/settings"); - const languageSelect = page.locator("select.language-select"); + const languageSelect = page.getByTestId("settings-language-select").locator("select"); const currentValue = await languageSelect.inputValue(); // Switch to the other language @@ -48,11 +47,11 @@ test.describe("Settings Page", () => { test("should show notification matrix", async ({ page }) => { await navigateTo(page, "/settings"); - const matrix = page.locator("div.notification-matrix"); + const matrix = page.getByTestId("settings-notification-matrix"); await expect(matrix).toBeVisible(); // Matrix contains toggle switches - const toggles = matrix.locator("label.toggle-switch"); + const toggles = matrix.locator('input[type="checkbox"]'); expect(await toggles.count()).toBeGreaterThanOrEqual(2); }); @@ -72,11 +71,8 @@ test.describe("Settings Page", () => { await navigateTo(page, "/settings"); - const emailSection = page - .locator(".setting-section") - .filter({ has: page.locator(".section-header h3").filter({ hasText: emailHeadingPattern }) }) - .first(); - const emailToggle = emailSection.locator('input[type="checkbox"]').first(); + const emailSection = page.getByTestId("settings-notification-card"); + const emailToggle = page.getByTestId("settings-email-enabled-toggle").locator('input[type="checkbox"]'); await expect(emailToggle).toBeDisabled(); await expect(emailSection.getByText(smtpUnavailablePattern)).toHaveCount(0); @@ -98,11 +94,8 @@ test.describe("Settings Page", () => { test.skip(!settingsResponse.ok, `Settings request failed with status ${settingsResponse.status}`); test.skip(!settingsResponse.body?.smtpHost, "SMTP is not configured in this environment"); - const emailSection = page - .locator(".setting-section") - .filter({ has: page.locator(".section-header h3").filter({ hasText: emailHeadingPattern }) }) - .first(); - const emailToggle = emailSection.locator('input[type="checkbox"]').first(); + const emailSection = page.getByTestId("settings-notification-card"); + const emailToggle = page.getByTestId("settings-email-enabled-toggle").locator('input[type="checkbox"]'); await expect(emailToggle).toBeEnabled(); await expect(emailSection.getByText(smtpUnavailablePattern)).toHaveCount(0); @@ -111,42 +104,44 @@ test.describe("Settings Page", () => { test("should show stock settings section with threshold inputs", async ({ page }) => { await navigateTo(page, "/settings"); - const thresholdGroup = page.locator("div.threshold-chips-group"); - await expect(thresholdGroup).toBeVisible(); - - // Should have three threshold number inputs - const thresholdInputs = thresholdGroup.locator('input[type="text"]'); - await expect(thresholdInputs).toHaveCount(3); + await expect(page.getByTestId("settings-security-card")).toBeVisible(); + await expect(page.getByTestId("settings-threshold-critical")).toBeVisible(); + await expect(page.getByTestId("settings-threshold-low")).toBeVisible(); + await expect(page.getByTestId("settings-threshold-high")).toBeVisible(); }); test("should show calculation mode radio cards", async ({ page }) => { await navigateTo(page, "/settings"); - const modeGroup = page.locator("div.calculation-mode-group"); + const modeGroup = page.getByTestId("settings-calculation-mode"); await expect(modeGroup).toBeVisible(); - - // Two radio cards: automatic and manual - const radioCards = modeGroup.locator("label.radio-card"); - await expect(radioCards).toHaveCount(2); - - // One should be selected - await expect(modeGroup.locator("label.radio-card.selected")).toHaveCount(1); + expect(await modeGroup.locator('[value="automatic"], [data-value="automatic"]').count()).toBeGreaterThan(0); + expect(await modeGroup.locator('[value="manual"], [data-value="manual"]').count()).toBeGreaterThan(0); }); test("should toggle calculation mode", async ({ page }) => { await navigateTo(page, "/settings"); - const modeGroup = page.locator("div.calculation-mode-group"); - const radioCards = modeGroup.locator("label.radio-card"); - await expect(radioCards).toHaveCount(2); - await expect(modeGroup.locator("label.radio-card.selected")).toHaveCount(1); + const modeGroup = page.getByTestId("settings-calculation-mode"); + const automatic = modeGroup.locator('input[type="radio"][value="automatic"]'); + const manual = modeGroup.locator('input[type="radio"][value="manual"]'); + await expect(automatic).toHaveCount(1); + await expect(manual).toHaveCount(1); + const automaticId = await automatic.getAttribute("id"); + const manualId = await manual.getAttribute("id"); + expect(automaticId).toBeTruthy(); + expect(manualId).toBeTruthy(); + const automaticLabel = modeGroup.locator(`label[for="${automaticId}"]`).first(); + const manualLabel = modeGroup.locator(`label[for="${manualId}"]`).first(); - const firstSelected = await radioCards.first().evaluate((el) => el.classList.contains("selected")); - const targetCard = firstSelected ? radioCards.nth(1) : radioCards.first(); - - await targetCard.click(); - await expect(targetCard).toHaveClass(/selected/, { timeout: 10000 }); - await expect(modeGroup.locator("label.radio-card.selected")).toHaveCount(1); + const automaticChecked = await automatic.isChecked(); + if (automaticChecked) { + await manualLabel.click(); + await expect(manual).toBeChecked(); + } else { + await automaticLabel.click(); + await expect(automatic).toBeChecked(); + } }); test("should have export action button", async ({ page }) => { @@ -181,78 +176,73 @@ test.describe("Settings Page", () => { test("should show export/import section", async ({ page }) => { await navigateTo(page, "/settings"); - // Export button - const exportBtn = page.locator("div.action-card button.secondary").first(); + const exportBtn = page + .getByTestId("settings-danger-zone-card") + .getByRole("button", { name: /Export Data|Daten exportieren/i }); await expect(exportBtn).toBeVisible(); }); test("should toggle a notification switch", async ({ page }) => { await navigateTo(page, "/settings"); - // Find all toggle-switch labels on the entire settings page - const allToggleLabels = page.locator("label.toggle-switch"); - const count = await allToggleLabels.count(); + const matrix = page.getByTestId("settings-notification-matrix"); + const toggles = matrix.locator('input[type="checkbox"]'); + const count = await toggles.count(); - // Find the first toggle that is NOT disabled - let enabledToggle = null; + let enabledToggle = null as null | ReturnType; for (let i = 0; i < count; i++) { - const label = allToggleLabels.nth(i); - const isDisabled = await label.evaluate((el) => el.classList.contains("disabled")); + const toggle = toggles.nth(i); + const isDisabled = !(await toggle.isEnabled()); if (!isDisabled) { - enabledToggle = label; + enabledToggle = toggle; break; } } test.skip(!enabledToggle, "All notification toggles are disabled in this environment"); - const checkbox = enabledToggle.locator('input[type="checkbox"]'); - const initialState = await checkbox.isChecked(); + const initialState = await enabledToggle.isChecked(); - // Click the label to toggle await enabledToggle.click(); if (initialState) { - await expect(checkbox).not.toBeChecked(); + await expect(enabledToggle).not.toBeChecked(); } else { - await expect(checkbox).toBeChecked(); + await expect(enabledToggle).toBeChecked(); } // Toggle back to restore original state await enabledToggle.click(); - await expect(checkbox).toHaveJSProperty("checked", initialState); + await expect(enabledToggle).toHaveJSProperty("checked", initialState); }); test("should validate stock thresholds", async ({ page }) => { await navigateTo(page, "/settings"); - const thresholdGroup = page.locator("div.threshold-chips-group"); - const inputs = thresholdGroup.locator('input[type="text"]'); - // Set an invalid value (critical > low) - const criticalInput = inputs.first(); + const criticalInput = page.getByTestId("settings-threshold-critical").locator("input"); await criticalInput.fill("999"); // Should show validation error - const validationError = page.locator("p.threshold-validation-error"); + const validationError = page.getByTestId("settings-threshold-validation"); await expect(validationError).toBeVisible(); }); test("should reach settings via user menu", async ({ page }) => { await navigateTo(page, "/dashboard"); - const userMenuButton = page.locator("button.user-menu-btn"); + const userMenuButton = page.getByTestId("user-menu-trigger"); test.skip(!(await userMenuButton.isVisible().catch(() => false)), "User menu is unavailable when auth is disabled"); // Open user menu await userMenuButton.click(); // Click settings option in dropdown - const settingsOption = page.locator(".user-dropdown").getByText(/Settings/i); + const settingsOption = page.getByTestId("user-menu-settings"); await expect(settingsOption).toBeVisible(); await settingsOption.click(); await expect(page).toHaveURL(/\/settings/); - await expect(page.locator("div.settings-form")).toBeVisible(); + await expect(page.getByTestId("settings-page")).toBeVisible(); }); });