// 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"]'); 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("/"); }); test("calendar page renders correctly on mobile viewport", async ({ page, }) => { // Navigate to calendar await page.goto("/calendar"); await page.waitForLoadState("networkidle"); // Verify viewport is still mobile size const viewportSize = page.viewportSize(); expect(viewportSize?.width).toBe(375); // Calendar page title heading should be visible (exact match to avoid "Calendar Subscription") const heading = page.getByRole("heading", { name: "Calendar", exact: true, }); await expect(heading).toBeVisible({ timeout: 10000 }); // Calendar grid should be visible const calendarGrid = page .getByRole("grid") .or(page.locator('[data-testid="month-view"]')); await expect(calendarGrid).toBeVisible({ timeout: 5000 }); // Month navigation should be visible const monthYear = page.getByText( /january|february|march|april|may|june|july|august|september|october|november|december/i, ); await expect(monthYear.first()).toBeVisible(); }); test("calendar day cells are touch-friendly on mobile", async ({ page, }) => { // Navigate to calendar await page.goto("/calendar"); await page.waitForLoadState("networkidle"); // Get day buttons const dayButtons = page.locator("button[data-day]"); const hasDayButtons = await dayButtons .first() .isVisible() .catch(() => false); if (!hasDayButtons) { test.skip(); return; } // Check that day buttons have reasonable tap target size // Per dashboard spec: "Touch-friendly 44x44px minimum tap targets" const firstDayButton = dayButtons.first(); const boundingBox = await firstDayButton.boundingBox(); if (boundingBox) { // Width and height should be at least 32px for touch targets // (some flexibility since mobile displays may compress slightly) expect(boundingBox.width).toBeGreaterThanOrEqual(32); expect(boundingBox.height).toBeGreaterThanOrEqual(32); } }); test("calendar navigation works on mobile", async ({ page }) => { // Navigate to calendar await page.goto("/calendar"); await page.waitForLoadState("networkidle"); // Find and click next month button const nextButton = page.getByRole("button", { name: /next|→|forward/i, }); const hasNext = await nextButton.isVisible().catch(() => false); if (hasNext) { // Click next await nextButton.click(); await page.waitForTimeout(500); // Calendar should still be functional after navigation const calendarGrid = page .getByRole("grid") .or(page.locator('[data-testid="month-view"]')); await expect(calendarGrid).toBeVisible(); // Month display should still be visible const monthYear = page.getByText( /january|february|march|april|may|june|july|august|september|october|november|december/i, ); await expect(monthYear.first()).toBeVisible(); } }); }); });