// ABOUTME: E2E tests for settings page including preferences and logout. // ABOUTME: Tests form rendering, validation, submission, and logout functionality. import { expect, test } from "@playwright/test"; test.describe("settings", () => { test.describe("unauthenticated", () => { test("redirects to login when not authenticated", async ({ page }) => { await page.goto("/settings"); // Should redirect to /login await expect(page).toHaveURL(/\/login/); }); test("garmin settings redirects to login when not authenticated", async ({ page, }) => { await page.goto("/settings/garmin"); // Should redirect to /login await expect(page).toHaveURL(/\/login/); }); }); test.describe("authenticated", () => { // These tests require TEST_USER_EMAIL and TEST_USER_PASSWORD env vars test.beforeEach(async ({ page }) => { const email = process.env.TEST_USER_EMAIL; const password = process.env.TEST_USER_PASSWORD; if (!email || !password) { test.skip(); return; } // Login via the login page await page.goto("/login"); await page.waitForLoadState("networkidle"); const emailInput = page.getByLabel(/email/i); const hasEmailForm = await emailInput.isVisible().catch(() => false); if (!hasEmailForm) { test.skip(); return; } await emailInput.fill(email); await page.getByLabel(/password/i).fill(password); await page.getByRole("button", { name: /sign in/i }).click(); // Wait for redirect to dashboard, then navigate to settings await page.waitForURL("/", { timeout: 10000 }); await page.goto("/settings"); await page.waitForLoadState("networkidle"); }); test("displays settings form with required fields", async ({ page }) => { // Check for cycle length input const cycleLengthInput = page.getByLabel(/cycle length/i); await expect(cycleLengthInput).toBeVisible(); // Check for notification time input const notificationTimeInput = page.getByLabel(/notification time/i); await expect(notificationTimeInput).toBeVisible(); // Check for timezone input const timezoneInput = page.getByLabel(/timezone/i); await expect(timezoneInput).toBeVisible(); }); test("shows save button", async ({ page }) => { const saveButton = page.getByRole("button", { name: /save/i }); await expect(saveButton).toBeVisible(); }); test("shows logout button", async ({ page }) => { const logoutButton = page.getByRole("button", { name: /log ?out/i }); await expect(logoutButton).toBeVisible(); }); test("shows link to garmin settings", async ({ page }) => { const garminLink = page.getByRole("link", { name: /manage|garmin/i }); await expect(garminLink).toBeVisible(); }); test("shows back to dashboard link", async ({ page }) => { const backLink = page.getByRole("link", { name: /back|dashboard/i }); await expect(backLink).toBeVisible(); }); test("can update cycle length", async ({ page }) => { const cycleLengthInput = page.getByLabel(/cycle length/i); // Clear and enter new value await cycleLengthInput.fill("30"); // Click save const saveButton = page.getByRole("button", { name: /save/i }); await saveButton.click(); // Should show success message or no error await page.waitForTimeout(1000); // Either success message or value persisted const errorMessage = page.locator('[role="alert"]').filter({ hasText: /error|failed/i, }); const hasError = await errorMessage.isVisible().catch(() => false); // No error means success expect(hasError).toBe(false); }); test("validates cycle length range", async ({ page }) => { const cycleLengthInput = page.getByLabel(/cycle length/i); // Enter invalid value (too low) await cycleLengthInput.fill("10"); // Click save const saveButton = page.getByRole("button", { name: /save/i }); await saveButton.click(); // Should show validation error or HTML5 validation await page.waitForTimeout(500); }); test("can navigate to garmin settings", async ({ page }) => { const garminLink = page.getByRole("link", { name: /manage|garmin/i }); await garminLink.click(); await expect(page).toHaveURL(/\/settings\/garmin/); }); test("can navigate back to dashboard", async ({ page }) => { const backLink = page.getByRole("link", { name: /back|dashboard/i }); await backLink.click(); await expect(page).toHaveURL("/"); }); test("logout redirects to login", async ({ page }) => { const logoutButton = page.getByRole("button", { name: /log ?out/i }); await logoutButton.click(); // Should redirect to login page await expect(page).toHaveURL(/\/login/, { timeout: 10000 }); }); }); test.describe("garmin settings", () => { test.beforeEach(async ({ page }) => { const email = process.env.TEST_USER_EMAIL; const password = process.env.TEST_USER_PASSWORD; if (!email || !password) { test.skip(); return; } // Login and navigate to garmin settings await page.goto("/login"); await page.waitForLoadState("networkidle"); const emailInput = page.getByLabel(/email/i); const hasEmailForm = await emailInput.isVisible().catch(() => false); if (!hasEmailForm) { test.skip(); return; } await emailInput.fill(email); await page.getByLabel(/password/i).fill(password); await page.getByRole("button", { name: /sign in/i }).click(); await page.waitForURL("/", { timeout: 10000 }); await page.goto("/settings/garmin"); await page.waitForLoadState("networkidle"); }); test("displays garmin connection status", async ({ page }) => { // Look for connection status indicator const statusText = page.getByText(/connected|not connected|status/i); await expect(statusText.first()).toBeVisible(); }); test("shows back navigation", async ({ page }) => { const backLink = page.getByRole("link", { name: /back|settings/i }); await expect(backLink).toBeVisible(); }); }); });