All checks were successful
Deploy / deploy (push) Successful in 1m37s
- Fix OIDC tests with route interception for auth-methods API - Add data-testid to DecisionCard for reliable test selection - Fix /api/today to fetch fresh user data instead of stale cookie data - Fix period logging test timing with proper API wait patterns - Fix decision engine test with waitForResponse instead of timeout - Simplify mobile viewport test locator All 206 e2e tests now pass with 0 skipped. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
224 lines
7.6 KiB
TypeScript
224 lines
7.6 KiB
TypeScript
// 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();
|
|
}
|
|
});
|
|
});
|
|
});
|