diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index f9bec6f..184de8c 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -1105,7 +1105,7 @@ These items were identified during gap analysis and have been completed. - **Files Created:** - `e2e/smoke.spec.ts` - 3 tests for basic app functionality - `e2e/auth.spec.ts` - 14 tests for login page, protected routes, public routes - - `e2e/dashboard.spec.ts` - 10 tests for dashboard display and overrides + - `e2e/dashboard.spec.ts` - 24 tests for dashboard display, overrides, data panels, nutrition, and accessibility (expanded from 10 tests) - `e2e/settings.spec.ts` - 15 tests for settings and Garmin configuration - `e2e/period-logging.spec.ts` - 9 tests for period history and API auth - `e2e/calendar.spec.ts` - 13 tests for calendar view and ICS endpoints @@ -1460,15 +1460,15 @@ This section outlines comprehensive e2e tests to cover the functionality describ #### Existing Files to Extend 1. `e2e/auth.spec.ts` - +6 tests -2. `e2e/dashboard.spec.ts` - +35 tests (largest expansion) +2. `e2e/dashboard.spec.ts` - +35 tests (+14 completed, +21 remaining) 3. `e2e/period-logging.spec.ts` - +5 tests 4. `e2e/calendar.spec.ts` - +13 tests 5. `e2e/settings.spec.ts` - +6 tests 6. `e2e/garmin.spec.ts` - +9 tests #### Total Test Count -- **Current E2E tests**: 100 tests (UPDATED: 36 new tests added across 5 new files) -- **New tests needed**: ~116 tests +- **Current E2E tests**: 114 tests (UPDATED: 36 new tests + 14 dashboard expansion) +- **New tests needed**: ~102 tests - **Across 15 test files** (7 existing + 8 new) #### Priority Order for Implementation @@ -1509,3 +1509,4 @@ This section outlines comprehensive e2e tests to cover the functionality describ 16. **Component Tests:** P3.11 COMPLETE - All 5 dashboard and calendar components now have comprehensive unit tests (90 tests total) 17. **Gap Analysis (2026-01-12):** Verified 977 tests across 50 files + 64 E2E tests across 6 files. All API routes (21), pages (8), components, and lib files (12) have tests. P0-P5 complete. Project is feature complete. 18. **E2E Test Expansion (2026-01-13):** Added 36 new E2E tests across 5 new files (health, history, plan, decision-engine, cycle). Total E2E coverage now 100 tests across 12 files. +19. **E2E Test Expansion (2026-01-13):** Added 14 new E2E tests to dashboard.spec.ts (8 data panel tests, 4 nutrition panel tests, 4 accessibility tests). Total dashboard E2E coverage now 24 tests. diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts index 8d0f66c..49bc176 100644 --- a/e2e/dashboard.spec.ts +++ b/e2e/dashboard.spec.ts @@ -180,6 +180,383 @@ test.describe("dashboard", () => { }); }); + test.describe("data panel", () => { + // 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("displays HRV status with label", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + // Wait for data panel to load + const dataPanel = page.locator('[data-testid="data-panel"]'); + const hasDataPanel = await dataPanel.isVisible().catch(() => false); + + if (hasDataPanel) { + // HRV status should show "Balanced", "Unbalanced", or "Unknown" + const hrvText = page.getByText(/HRV Status/i); + await expect(hrvText).toBeVisible(); + + const statusText = page.getByText(/balanced|unbalanced|unknown/i); + const hasStatus = await statusText + .first() + .isVisible() + .catch(() => false); + expect(hasStatus).toBe(true); + } + }); + + test("displays Body Battery current value", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + const dataPanel = page.locator('[data-testid="data-panel"]'); + const hasDataPanel = await dataPanel.isVisible().catch(() => false); + + if (hasDataPanel) { + // Body Battery label should be visible + const bbLabel = page.getByText(/Body Battery/i).first(); + await expect(bbLabel).toBeVisible(); + } + }); + + test("displays cycle day in 'Day X' format", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + // Look for "Day" followed by a number + const cycleDayText = page.getByText(/Day \d+/i); + const hasCycleDay = await cycleDayText + .first() + .isVisible() + .catch(() => false); + + // Either has cycle day or onboarding (both valid states) + if (!hasCycleDay) { + const onboarding = page.getByText(/set.*period|log.*period/i); + const hasOnboarding = await onboarding + .first() + .isVisible() + .catch(() => false); + expect(hasCycleDay || hasOnboarding).toBe(true); + } + }); + + test("displays current phase name", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + // Look for phase names + const phaseNames = [ + "MENSTRUAL", + "FOLLICULAR", + "OVULATION", + "EARLY_LUTEAL", + "LATE_LUTEAL", + ]; + let foundPhase = false; + + for (const phase of phaseNames) { + const phaseText = page.getByText(new RegExp(phase, "i")); + const isVisible = await phaseText + .first() + .isVisible() + .catch(() => false); + if (isVisible) { + foundPhase = true; + break; + } + } + + // Either has phase or shows onboarding + if (!foundPhase) { + const onboarding = page.getByText(/set.*period|log.*period/i); + const hasOnboarding = await onboarding + .first() + .isVisible() + .catch(() => false); + expect(foundPhase || hasOnboarding).toBe(true); + } + }); + + test("displays week intensity minutes", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + const dataPanel = page.locator('[data-testid="data-panel"]'); + const hasDataPanel = await dataPanel.isVisible().catch(() => false); + + if (hasDataPanel) { + // Look for intensity-related text + const intensityLabel = page.getByText(/intensity|minutes/i); + const hasIntensity = await intensityLabel + .first() + .isVisible() + .catch(() => false); + expect(hasIntensity).toBe(true); + } + }); + + test("displays phase limit", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + const dataPanel = page.locator('[data-testid="data-panel"]'); + const hasDataPanel = await dataPanel.isVisible().catch(() => false); + + if (hasDataPanel) { + // Phase limit should be shown as a number (minutes) + const limitLabel = page.getByText(/limit|remaining/i); + const hasLimit = await limitLabel + .first() + .isVisible() + .catch(() => false); + expect(hasLimit).toBe(true); + } + }); + + test("displays remaining minutes calculation", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + const dataPanel = page.locator('[data-testid="data-panel"]'); + const hasDataPanel = await dataPanel.isVisible().catch(() => false); + + if (hasDataPanel) { + // Remaining minutes should show (phase limit - week intensity) + const remainingLabel = page.getByText(/remaining/i); + const hasRemaining = await remainingLabel + .first() + .isVisible() + .catch(() => false); + expect(hasRemaining).toBe(true); + } + }); + }); + + test.describe("nutrition panel", () => { + 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("displays seed cycling recommendation", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + // Look for seed names (flax, pumpkin, sesame, sunflower) + const seedText = page.getByText(/flax|pumpkin|sesame|sunflower/i); + const hasSeeds = await seedText + .first() + .isVisible() + .catch(() => false); + + // Either has seeds info or onboarding + if (!hasSeeds) { + const onboarding = page.getByText(/set.*period|log.*period/i); + const hasOnboarding = await onboarding + .first() + .isVisible() + .catch(() => false); + expect(hasSeeds || hasOnboarding).toBe(true); + } + }); + + test("displays carbohydrate range", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + // Look for carb-related text + const carbText = page.getByText(/carb|carbohydrate/i); + const hasCarbs = await carbText + .first() + .isVisible() + .catch(() => false); + + if (!hasCarbs) { + const onboarding = page.getByText(/set.*period|log.*period/i); + const hasOnboarding = await onboarding + .first() + .isVisible() + .catch(() => false); + expect(hasCarbs || hasOnboarding).toBe(true); + } + }); + + test("displays keto guidance", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + // Look for keto-related text + const ketoText = page.getByText(/keto/i); + const hasKeto = await ketoText + .first() + .isVisible() + .catch(() => false); + + if (!hasKeto) { + const onboarding = page.getByText(/set.*period|log.*period/i); + const hasOnboarding = await onboarding + .first() + .isVisible() + .catch(() => false); + expect(hasKeto || hasOnboarding).toBe(true); + } + }); + + test("displays nutrition section header", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + // Nutrition panel should have a header + const nutritionHeader = page.getByRole("heading", { name: /nutrition/i }); + const hasHeader = await nutritionHeader.isVisible().catch(() => false); + + if (!hasHeader) { + // May be text label instead of heading + const nutritionText = page.getByText(/nutrition/i); + const hasText = await nutritionText + .first() + .isVisible() + .catch(() => false); + expect(hasHeader || hasText).toBe(true); + } + }); + }); + + test.describe("accessibility", () => { + 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("dashboard has main landmark", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + const mainElement = page.locator("main"); + await expect(mainElement).toBeVisible(); + }); + + test("skip navigation link is available", async ({ page }) => { + // Skip link should be present (may be visually hidden until focused) + const skipLink = page.getByRole("link", { name: /skip to main/i }); + + // Check if it exists in DOM even if visually hidden + const skipLinkExists = await skipLink.count(); + expect(skipLinkExists).toBeGreaterThan(0); + }); + + test("override toggles are keyboard accessible", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + const overridesHeading = page.getByRole("heading", { name: "OVERRIDES" }); + const hasOverrides = await overridesHeading + .waitFor({ timeout: 10000 }) + .then(() => true) + .catch(() => false); + + if (!hasOverrides) { + test.skip(); + return; + } + + // Find a checkbox + const checkbox = page.getByRole("checkbox").first(); + const hasCheckbox = await checkbox.isVisible().catch(() => false); + + if (hasCheckbox) { + // Focus should be possible via tab + await checkbox.focus(); + const isFocused = await checkbox.evaluate( + (el) => document.activeElement === el, + ); + expect(isFocused).toBe(true); + } + }); + + test("interactive elements have focus indicators", async ({ page }) => { + await page.waitForLoadState("networkidle"); + + // Find any link or button + const interactiveElement = page + .getByRole("link") + .or(page.getByRole("button")) + .first(); + const hasInteractive = await interactiveElement + .isVisible() + .catch(() => false); + + if (hasInteractive) { + // Focus the element + await interactiveElement.focus(); + + // Element should receive focus (we can't easily test visual ring, but focus should work) + const isFocused = await interactiveElement.evaluate( + (el) => document.activeElement === el, + ); + expect(isFocused).toBe(true); + } + }); + }); + test.describe("error handling", () => { test("handles network errors gracefully", async ({ page }) => { // Intercept API calls and make them fail