All checks were successful
Deploy / deploy (push) Successful in 2m28s
Period logging tests (5 new): - Future date validation - Cycle length display between periods - Prediction accuracy display - Delete period log from history - Edit period log from history Calendar tests (8 new): - Today highlight in calendar view - Phase colors in calendar days - Phase legend display - Today button for quick navigation - Multi-month navigation with return to today - Calendar URL generation - URL format validation - Copy to clipboard functionality Total E2E tests: 113 (was 100) Total unit tests: 1014 (51 test files) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
290 lines
9.0 KiB
TypeScript
290 lines
9.0 KiB
TypeScript
// ABOUTME: E2E tests for period logging functionality.
|
|
// ABOUTME: Tests period start logging, date selection, and period history.
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
test.describe("period logging", () => {
|
|
test.describe("unauthenticated", () => {
|
|
test("period history page redirects to login when not authenticated", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/period-history");
|
|
|
|
// Should redirect to /login
|
|
await expect(page).toHaveURL(/\/login/);
|
|
});
|
|
});
|
|
|
|
test.describe("authenticated", () => {
|
|
// 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;
|
|
}
|
|
|
|
// 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();
|
|
|
|
await page.waitForURL("/", { timeout: 10000 });
|
|
});
|
|
|
|
test("dashboard shows period date prompt for new users", async ({
|
|
page,
|
|
}) => {
|
|
// Check if onboarding banner for period date is visible
|
|
// This depends on whether the test user has period data set
|
|
const onboardingBanner = page.getByText(
|
|
/period|log your period|set.*date/i,
|
|
);
|
|
const hasOnboarding = await onboardingBanner
|
|
.first()
|
|
.isVisible()
|
|
.catch(() => false);
|
|
|
|
// Either has onboarding prompt or has cycle data - both are valid states
|
|
if (hasOnboarding) {
|
|
await expect(onboardingBanner.first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test("period history page is accessible", async ({ page }) => {
|
|
await page.goto("/period-history");
|
|
|
|
// Should show period history content
|
|
await expect(page.getByRole("heading")).toBeVisible();
|
|
});
|
|
|
|
test("period history shows table or empty state", async ({ page }) => {
|
|
await page.goto("/period-history");
|
|
|
|
// Wait for loading to complete
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Look for either table or empty state message
|
|
const table = page.getByRole("table");
|
|
const emptyState = page.getByText("No period history found");
|
|
const totalText = page.getByText(/\d+ periods/);
|
|
|
|
const hasTable = await table.isVisible().catch(() => false);
|
|
const hasEmpty = await emptyState.isVisible().catch(() => false);
|
|
const hasTotal = await totalText.isVisible().catch(() => false);
|
|
|
|
// Either table, empty state, or total count should be present
|
|
expect(hasTable || hasEmpty || hasTotal).toBe(true);
|
|
});
|
|
|
|
test("period history shows average cycle length if data exists", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/period-history");
|
|
|
|
// Average cycle length is shown when there's enough data
|
|
const avgText = page.getByText(/average.*cycle|cycle.*average|avg/i);
|
|
const hasAvg = await avgText
|
|
.first()
|
|
.isVisible()
|
|
.catch(() => false);
|
|
|
|
// This is optional - depends on having data
|
|
if (hasAvg) {
|
|
await expect(avgText.first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test("period history shows back navigation", async ({ page }) => {
|
|
await page.goto("/period-history");
|
|
|
|
// Look for back link
|
|
const backLink = page.getByRole("link", { name: /back|dashboard|home/i });
|
|
await expect(backLink).toBeVisible();
|
|
});
|
|
|
|
test("can navigate to period history from dashboard", async ({ page }) => {
|
|
// Look for navigation to period history
|
|
const periodHistoryLink = page.getByRole("link", {
|
|
name: /period.*history|history/i,
|
|
});
|
|
const hasLink = await periodHistoryLink.isVisible().catch(() => false);
|
|
|
|
if (hasLink) {
|
|
await periodHistoryLink.click();
|
|
await expect(page).toHaveURL(/\/period-history/);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe("API endpoints", () => {
|
|
test("period history API requires authentication", async ({ page }) => {
|
|
const response = await page.request.get("/api/period-history");
|
|
|
|
// Should return 401 Unauthorized
|
|
expect(response.status()).toBe(401);
|
|
});
|
|
|
|
test("period log API requires authentication", async ({ page }) => {
|
|
const response = await page.request.post("/api/cycle/period", {
|
|
data: { startDate: "2024-01-15" },
|
|
});
|
|
|
|
// Should return 401 Unauthorized
|
|
expect(response.status()).toBe(401);
|
|
});
|
|
});
|
|
|
|
test.describe("period logging flow", () => {
|
|
// 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("period date cannot be in the future", async ({ page }) => {
|
|
// Navigate to period history
|
|
await page.goto("/period-history");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Look for an "Add Period" or "Log Period" button
|
|
const addButton = page.getByRole("button", {
|
|
name: /add.*period|log.*period|new.*period/i,
|
|
});
|
|
const hasAddButton = await addButton.isVisible().catch(() => false);
|
|
|
|
if (!hasAddButton) {
|
|
// Try dashboard - look for period logging modal trigger
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const periodButton = page.getByRole("button", {
|
|
name: /log.*period|add.*period/i,
|
|
});
|
|
const hasPeriodButton = await periodButton
|
|
.isVisible()
|
|
.catch(() => false);
|
|
|
|
if (!hasPeriodButton) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
test("period history displays cycle length between periods", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/period-history");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Look for cycle length column or text
|
|
const cycleLengthText = page.getByText(/cycle.*length|\d+\s*days/i);
|
|
const hasCycleLength = await cycleLengthText
|
|
.first()
|
|
.isVisible()
|
|
.catch(() => false);
|
|
|
|
// If there's period data, cycle length should be visible
|
|
const table = page.getByRole("table");
|
|
const hasTable = await table.isVisible().catch(() => false);
|
|
|
|
if (hasTable) {
|
|
// Table has header for cycle length
|
|
const header = page.getByRole("columnheader", {
|
|
name: /cycle.*length|days/i,
|
|
});
|
|
const hasHeader = await header.isVisible().catch(() => false);
|
|
expect(hasHeader || hasCycleLength).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("period history shows prediction accuracy when available", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/period-history");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Look for prediction-related text (early/late, accuracy)
|
|
const predictionText = page.getByText(/early|late|accuracy|predicted/i);
|
|
const hasPrediction = await predictionText
|
|
.first()
|
|
.isVisible()
|
|
.catch(() => false);
|
|
|
|
// Prediction info may not be visible if not enough data
|
|
if (hasPrediction) {
|
|
await expect(predictionText.first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test("can delete period log from history", async ({ page }) => {
|
|
await page.goto("/period-history");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Look for delete button
|
|
const deleteButton = page.getByRole("button", { name: /delete/i });
|
|
const hasDelete = await deleteButton
|
|
.first()
|
|
.isVisible()
|
|
.catch(() => false);
|
|
|
|
if (hasDelete) {
|
|
// Delete button exists for period entries
|
|
await expect(deleteButton.first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test("can edit period log from history", async ({ page }) => {
|
|
await page.goto("/period-history");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Look for edit button
|
|
const editButton = page.getByRole("button", { name: /edit/i });
|
|
const hasEdit = await editButton
|
|
.first()
|
|
.isVisible()
|
|
.catch(() => false);
|
|
|
|
if (hasEdit) {
|
|
// Edit button exists for period entries
|
|
await expect(editButton.first()).toBeVisible();
|
|
}
|
|
});
|
|
});
|
|
});
|