diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 7403bc9..69bda56 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -4,7 +4,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta ## Current Status: Feature Complete -**Test Coverage:** 1014 unit tests (51 files) + 186 E2E tests (12 files) = 1200 total tests +**Test Coverage:** 1014 unit tests (51 files) + 190 E2E tests (13 files) = 1204 total tests All P0-P5 items are complete. The project is feature complete. @@ -97,7 +97,7 @@ All P0-P5 items are complete. The project is feature complete. | PeriodDateModal | 22 | Period input modal | | Skeletons | 29 | Loading states with shimmer | -### E2E Tests (12 files, 186 tests) +### E2E Tests (13 files, 190 tests) | File | Tests | Coverage | |------|-------|----------| | smoke.spec.ts | 3 | Basic app functionality | @@ -112,6 +112,7 @@ All P0-P5 items are complete. The project is feature complete. | history.spec.ts | 7 | History page | | plan.spec.ts | 7 | Plan page | | health.spec.ts | 3 | Health/observability | +| mobile.spec.ts | 4 | Mobile viewport behavior, responsive layout | --- @@ -124,7 +125,6 @@ These are optional enhancements to improve E2E coverage. Not required for featur |------|-------|-------------| | notifications.spec.ts | 3 | Notification preferences | | dark-mode.spec.ts | 2 | System preference detection | -| mobile.spec.ts | 4 | Mobile viewport behavior | ### Existing File Extensions | File | Additional Tests | Focus Area | @@ -148,6 +148,7 @@ These are optional enhancements to improve E2E coverage. Not required for featur ## Revision History +- 2026-01-13: Added mobile.spec.ts with 4 E2E tests (mobile viewport behavior, responsive layout) - 2026-01-13: Added 6 auth E2E tests (OIDC button display, loading states, session persistence across pages/refresh) - 2026-01-13: Added 5 settings persistence E2E tests (notification time, timezone, multi-field persistence) - 2026-01-13: Added 5 period-logging E2E tests (modal flow, future date restriction, edit/delete flows) diff --git a/e2e/mobile.spec.ts b/e2e/mobile.spec.ts new file mode 100644 index 0000000..eb75698 --- /dev/null +++ b/e2e/mobile.spec.ts @@ -0,0 +1,132 @@ +// ABOUTME: E2E tests for mobile viewport behavior and responsive design. +// ABOUTME: Tests that the dashboard displays correctly on mobile-sized screens. +import { expect, test } from "@playwright/test"; + +// Mobile viewport: iPhone SE/8 (375x667) +const MOBILE_VIEWPORT = { width: 375, height: 667 }; + +test.describe("mobile viewport", () => { + test.describe("unauthenticated", () => { + test("login page renders correctly on mobile viewport", async ({ + page, + }) => { + await page.setViewportSize(MOBILE_VIEWPORT); + await page.goto("/login"); + + // Login form should be visible - heading is "PhaseFlow" + const heading = page.getByRole("heading", { name: /phaseflow/i }); + await expect(heading).toBeVisible(); + + // Email input should be visible + const emailInput = page.getByLabel(/email/i); + await expect(emailInput).toBeVisible(); + + // Viewport width should be mobile + const viewportSize = page.viewportSize(); + expect(viewportSize?.width).toBe(375); + }); + }); + + test.describe("authenticated", () => { + test.beforeEach(async ({ page }) => { + const email = process.env.TEST_USER_EMAIL; + const password = process.env.TEST_USER_PASSWORD; + + if (!email || !password) { + test.skip(); + return; + } + + // Set mobile viewport before navigating + await page.setViewportSize(MOBILE_VIEWPORT); + + // 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 + await page.waitForURL("/", { timeout: 10000 }); + await page.waitForLoadState("networkidle"); + }); + + test("dashboard displays correctly on mobile viewport", async ({ + page, + }) => { + // Verify viewport is mobile size + const viewportSize = page.viewportSize(); + expect(viewportSize?.width).toBe(375); + + // Header should be visible + const header = page.getByRole("heading", { name: /phaseflow/i }); + await expect(header).toBeVisible(); + + // Settings link should be visible + const settingsLink = page.getByRole("link", { name: /settings/i }); + await expect(settingsLink).toBeVisible(); + + // Decision card should be visible + const decisionCard = page + .locator('[data-testid="decision-card"]') + .or(page.getByText(/rest|gentle|light|reduced|train/i).first()); + await expect(decisionCard).toBeVisible(); + + // Data panel should be visible + const dataPanel = page.getByText(/body battery|hrv/i).first(); + await expect(dataPanel).toBeVisible(); + }); + + test("dashboard uses single-column layout on mobile", async ({ page }) => { + // On mobile (<768px), the Data Panel and Nutrition Panel should stack vertically + // This is controlled by the md:grid-cols-2 class + + // Find the grid container that holds Data Panel and Nutrition Panel + // It should NOT have two-column grid on mobile (should be single column) + const gridContainer = page.locator(".grid.gap-4").first(); + const containerExists = await gridContainer + .isVisible() + .catch(() => false); + + if (containerExists) { + // Get the computed grid template columns + const gridTemplateColumns = await gridContainer.evaluate((el) => { + return window.getComputedStyle(el).gridTemplateColumns; + }); + + // On mobile (375px < 768px), should NOT be two columns + // Single column would be "none" or a single value like "1fr" + // Two columns would be something like "1fr 1fr" or "repeat(2, 1fr)" + const isTwoColumn = gridTemplateColumns.includes(" "); + expect(isTwoColumn).toBe(false); + } + }); + + test("navigation elements are interactive on mobile", async ({ page }) => { + // Settings link should be clickable + const settingsLink = page.getByRole("link", { name: /settings/i }); + await expect(settingsLink).toBeVisible(); + + // Click settings and verify navigation + await settingsLink.click(); + await expect(page).toHaveURL(/\/settings/); + + // Back button should work to return to dashboard + const backLink = page.getByRole("link", { name: /back|dashboard|home/i }); + await expect(backLink).toBeVisible(); + await backLink.click(); + + await expect(page).toHaveURL("/"); + }); + }); +});