// 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("/"); }); }); });