// ABOUTME: E2E tests for period logging functionality. // ABOUTME: Tests period start logging, date selection, and period history. import { expect, test } from "@playwright/test"; test.describe("period logging", () => { test.describe("unauthenticated", () => { test("period history page redirects to login when not authenticated", async ({ page, }) => { await page.goto("/period-history"); // 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(); await page.waitForURL("/", { timeout: 10000 }); }); test("dashboard shows period date prompt for new users", async ({ page, }) => { // Check if onboarding banner for period date is visible // This depends on whether the test user has period data set const onboardingBanner = page.getByText( /period|log your period|set.*date/i, ); const hasOnboarding = await onboardingBanner .first() .isVisible() .catch(() => false); // Either has onboarding prompt or has cycle data - both are valid states if (hasOnboarding) { await expect(onboardingBanner.first()).toBeVisible(); } }); test("period history page is accessible", async ({ page }) => { await page.goto("/period-history"); // Should show period history content await expect(page.getByRole("heading")).toBeVisible(); }); test("period history shows table or empty state", async ({ page }) => { await page.goto("/period-history"); // Wait for loading to complete await page.waitForLoadState("networkidle"); // Look for either table or empty state message const table = page.getByRole("table"); const emptyState = page.getByText("No period history found"); const totalText = page.getByText(/\d+ periods/); const hasTable = await table.isVisible().catch(() => false); const hasEmpty = await emptyState.isVisible().catch(() => false); const hasTotal = await totalText.isVisible().catch(() => false); // Either table, empty state, or total count should be present expect(hasTable || hasEmpty || hasTotal).toBe(true); }); test("period history shows average cycle length if data exists", async ({ page, }) => { await page.goto("/period-history"); // Average cycle length is shown when there's enough data const avgText = page.getByText(/average.*cycle|cycle.*average|avg/i); const hasAvg = await avgText .first() .isVisible() .catch(() => false); // This is optional - depends on having data if (hasAvg) { await expect(avgText.first()).toBeVisible(); } }); test("period history shows back navigation", async ({ page }) => { await page.goto("/period-history"); // Look for back link const backLink = page.getByRole("link", { name: /back|dashboard|home/i }); await expect(backLink).toBeVisible(); }); test("can navigate to period history from dashboard", async ({ page }) => { // Look for navigation to period history const periodHistoryLink = page.getByRole("link", { name: /period.*history|history/i, }); const hasLink = await periodHistoryLink.isVisible().catch(() => false); if (hasLink) { await periodHistoryLink.click(); await expect(page).toHaveURL(/\/period-history/); } }); }); test.describe("API endpoints", () => { test("period history API requires authentication", async ({ page }) => { const response = await page.request.get("/api/period-history"); // Should return 401 Unauthorized expect(response.status()).toBe(401); }); test("period log API requires authentication", async ({ page }) => { const response = await page.request.post("/api/cycle/period", { data: { startDate: "2024-01-15" }, }); // Should return 401 Unauthorized expect(response.status()).toBe(401); }); }); test.describe("period logging flow", () => { // 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; } 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 }); }); test("period date cannot be in the future", async ({ page }) => { // Navigate to period history await page.goto("/period-history"); await page.waitForLoadState("networkidle"); // Look for an "Add Period" or "Log Period" button const addButton = page.getByRole("button", { name: /add.*period|log.*period|new.*period/i, }); const hasAddButton = await addButton.isVisible().catch(() => false); if (!hasAddButton) { // Try dashboard - look for period logging modal trigger await page.goto("/"); await page.waitForLoadState("networkidle"); const periodButton = page.getByRole("button", { name: /log.*period|add.*period/i, }); const hasPeriodButton = await periodButton .isVisible() .catch(() => false); if (!hasPeriodButton) { test.skip(); return; } } }); test("period history displays cycle length between periods", async ({ page, }) => { await page.goto("/period-history"); await page.waitForLoadState("networkidle"); // Look for cycle length column or text const cycleLengthText = page.getByText(/cycle.*length|\d+\s*days/i); const hasCycleLength = await cycleLengthText .first() .isVisible() .catch(() => false); // If there's period data, cycle length should be visible const table = page.getByRole("table"); const hasTable = await table.isVisible().catch(() => false); if (hasTable) { // Table has header for cycle length const header = page.getByRole("columnheader", { name: /cycle.*length|days/i, }); const hasHeader = await header.isVisible().catch(() => false); expect(hasHeader || hasCycleLength).toBe(true); } }); test("period history shows prediction accuracy when available", async ({ page, }) => { await page.goto("/period-history"); await page.waitForLoadState("networkidle"); // Look for prediction-related text (early/late, accuracy) const predictionText = page.getByText(/early|late|accuracy|predicted/i); const hasPrediction = await predictionText .first() .isVisible() .catch(() => false); // Prediction info may not be visible if not enough data if (hasPrediction) { await expect(predictionText.first()).toBeVisible(); } }); test("can delete period log from history", async ({ page }) => { await page.goto("/period-history"); await page.waitForLoadState("networkidle"); // Look for delete button const deleteButton = page.getByRole("button", { name: /delete/i }); const hasDelete = await deleteButton .first() .isVisible() .catch(() => false); if (hasDelete) { // Delete button exists for period entries await expect(deleteButton.first()).toBeVisible(); } }); test("can edit period log from history", async ({ page }) => { await page.goto("/period-history"); await page.waitForLoadState("networkidle"); // Look for edit button const editButton = page.getByRole("button", { name: /edit/i }); const hasEdit = await editButton .first() .isVisible() .catch(() => false); if (hasEdit) { // Edit button exists for period entries await expect(editButton.first()).toBeVisible(); } }); }); });