diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index d808c29..9ecb9b3 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) + 170 E2E tests (12 files) = 1184 total tests +**Test Coverage:** 1014 unit tests (51 files) + 175 E2E tests (12 files) = 1189 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, 165 tests) +### E2E Tests (12 files, 175 tests) | File | Tests | Coverage | |------|-------|----------| | smoke.spec.ts | 3 | Basic app functionality | @@ -105,7 +105,7 @@ All P0-P5 items are complete. The project is feature complete. | dashboard.spec.ts | 24 | Dashboard display, overrides | | settings.spec.ts | 15 | Settings form, validation | | garmin.spec.ts | 12 | Garmin connection, expiry warnings | -| period-logging.spec.ts | 14 | Period history, logging | +| period-logging.spec.ts | 19 | Period history, logging, modal flows | | calendar.spec.ts | 21 | Calendar view, ICS feed | | decision-engine.spec.ts | 8 | Decision priority chain | | cycle.spec.ts | 11 | Cycle tracking | @@ -130,7 +130,6 @@ These are optional enhancements to improve E2E coverage. Not required for featur | File | Additional Tests | Focus Area | |------|------------------|------------| | auth.spec.ts | +6 | OIDC flow, session persistence | -| period-logging.spec.ts | +5 | Future dates, dashboard updates | | calendar.spec.ts | +13 | ICS content validation, responsive | | settings.spec.ts | +6 | Persistence, timezone changes | | garmin.spec.ts | +4 | Token refresh, network error recovery | @@ -150,6 +149,7 @@ These are optional enhancements to improve E2E coverage. Not required for featur ## Revision History +- 2026-01-13: Added 5 period-logging E2E tests (modal flow, future date restriction, edit/delete flows) - 2026-01-13: Added 5 Garmin E2E tests (expiry warnings, expired state, persistence, reconnection) - 2026-01-13: Condensed plan after feature completion (reduced from 1514 to ~170 lines) - 2026-01-12: Fixed spec gaps (email format, HRV colors, progress bar, emojis) diff --git a/e2e/period-logging.spec.ts b/e2e/period-logging.spec.ts index 0ccf9b9..9153dbe 100644 --- a/e2e/period-logging.spec.ts +++ b/e2e/period-logging.spec.ts @@ -285,5 +285,264 @@ test.describe("period logging", () => { await expect(editButton.first()).toBeVisible(); } }); + + test("period date modal opens from dashboard onboarding banner", async ({ + page, + }) => { + // Look for the "Set date" button in onboarding banner + const setDateButton = page.getByRole("button", { name: /set date/i }); + const hasSetDate = await setDateButton.isVisible().catch(() => false); + + if (!hasSetDate) { + // User may already have period date set - skip if no onboarding banner + test.skip(); + return; + } + + // Click the set date button + await setDateButton.click(); + + // Modal should open with "Set Period Date" title + const modalTitle = page.getByRole("heading", { + name: /set period date/i, + }); + await expect(modalTitle).toBeVisible(); + + // Should have a date input + const dateInput = page.locator('input[type="date"]'); + await expect(dateInput).toBeVisible(); + + // Should have Cancel and Save buttons + await expect(page.getByRole("button", { name: /cancel/i })).toBeVisible(); + await expect(page.getByRole("button", { name: /save/i })).toBeVisible(); + + // Cancel should close the modal + await page.getByRole("button", { name: /cancel/i }).click(); + await expect(modalTitle).not.toBeVisible(); + }); + + test("period date input restricts future dates", async ({ page }) => { + // Look for the "Set date" button in onboarding banner + const setDateButton = page.getByRole("button", { name: /set date/i }); + const hasSetDate = await setDateButton.isVisible().catch(() => false); + + if (!hasSetDate) { + test.skip(); + return; + } + + // Open the modal + await setDateButton.click(); + + // Get the date input and check its max attribute + const dateInput = page.locator('input[type="date"]'); + await expect(dateInput).toBeVisible(); + + // The max attribute should be set to today's date (YYYY-MM-DD format) + const maxValue = await dateInput.getAttribute("max"); + expect(maxValue).toBeTruthy(); + + // Parse max date and verify it's today or earlier + const maxDate = new Date(maxValue as string); + const today = new Date(); + today.setHours(0, 0, 0, 0); + maxDate.setHours(0, 0, 0, 0); + + expect(maxDate.getTime()).toBeLessThanOrEqual(today.getTime()); + + // Close modal + await page.getByRole("button", { name: /cancel/i }).click(); + }); + + test("logging period from modal updates dashboard cycle info", async ({ + page, + }) => { + // Look for the "Set date" button in onboarding banner + const setDateButton = page.getByRole("button", { name: /set date/i }); + const hasSetDate = await setDateButton.isVisible().catch(() => false); + + if (!hasSetDate) { + // User may already have period date set - skip if no onboarding banner + test.skip(); + return; + } + + // Click the set date button + await setDateButton.click(); + + // Wait for modal to be visible + const modalTitle = page.getByRole("heading", { + name: /set period date/i, + }); + await expect(modalTitle).toBeVisible(); + + // Calculate a valid date (7 days ago) + const testDate = new Date(); + testDate.setDate(testDate.getDate() - 7); + const dateStr = testDate.toISOString().split("T")[0]; + + // Fill in the date + const dateInput = page.locator('input[type="date"]'); + await dateInput.fill(dateStr); + + // Click Save + await page.getByRole("button", { name: /save/i }).click(); + + // Modal should close + await expect(modalTitle).not.toBeVisible(); + + // Dashboard should now show cycle information (Day X · Phase) + await page.waitForLoadState("networkidle"); + + // Look for cycle day display (e.g., "Day 8 · Follicular" or similar) + const cycleInfo = page.getByText(/day\s+\d+\s+·/i); + await expect(cycleInfo).toBeVisible({ timeout: 10000 }); + }); + + test("edit period modal flow changes date successfully", async ({ + page, + }) => { + await page.goto("/period-history"); + await page.waitForLoadState("networkidle"); + + // Look for edit button and table to ensure we have data + const editButton = page.getByRole("button", { name: /edit/i }).first(); + const hasEdit = await editButton.isVisible().catch(() => false); + + if (!hasEdit) { + test.skip(); + return; + } + + // Get the original date from the first row + const firstRow = page.locator("tbody tr").first(); + const originalDateCell = firstRow.locator("td").first(); + const originalDateText = await originalDateCell.textContent(); + + // Click edit button + await editButton.click(); + + // Edit modal should appear + const editModalTitle = page.getByRole("heading", { + name: /edit period date/i, + }); + await expect(editModalTitle).toBeVisible(); + + // Get the date input in the edit modal + const editDateInput = page.locator("#editDate"); + await expect(editDateInput).toBeVisible(); + + // Calculate a new date (14 days ago) + const newDate = new Date(); + newDate.setDate(newDate.getDate() - 14); + const newDateStr = newDate.toISOString().split("T")[0]; + + // Clear and fill new date + await editDateInput.fill(newDateStr); + + // Click Save in the edit modal + await page.getByRole("button", { name: /save/i }).click(); + + // Modal should close + await expect(editModalTitle).not.toBeVisible(); + + // Wait for table to refresh + await page.waitForLoadState("networkidle"); + + // Verify the date changed (the row should have new date text) + const updatedDateCell = page + .locator("tbody tr") + .first() + .locator("td") + .first(); + const updatedDateText = await updatedDateCell.textContent(); + + // If we had original data, verify it changed + if (originalDateText) { + // Format the new date to match display format (e.g., "Jan 1, 2024") + const formattedNewDate = newDate.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + expect(updatedDateText).toContain( + formattedNewDate.split(",")[0].split(" ")[0], + ); + } + }); + + test("delete period confirmation flow removes entry", async ({ page }) => { + await page.goto("/period-history"); + await page.waitForLoadState("networkidle"); + + // Look for delete button + const deleteButton = page + .getByRole("button", { name: /delete/i }) + .first(); + const hasDelete = await deleteButton.isVisible().catch(() => false); + + if (!hasDelete) { + test.skip(); + return; + } + + // Get the total count text before deletion + const totalText = page.getByText(/\d+ periods/); + const hasTotal = await totalText.isVisible().catch(() => false); + let originalCount = 0; + if (hasTotal) { + const countMatch = (await totalText.textContent())?.match( + /(\d+) periods/, + ); + if (countMatch) { + originalCount = parseInt(countMatch[1], 10); + } + } + + // Click delete button + await deleteButton.click(); + + // Confirmation modal should appear + const confirmModalTitle = page.getByRole("heading", { + name: /delete period/i, + }); + await expect(confirmModalTitle).toBeVisible(); + + // Should show warning message + const warningText = page.getByText(/are you sure.*delete/i); + await expect(warningText).toBeVisible(); + + // Should have Cancel and Confirm buttons + await expect(page.getByRole("button", { name: /cancel/i })).toBeVisible(); + await expect( + page.getByRole("button", { name: /confirm/i }), + ).toBeVisible(); + + // Click Confirm to delete + await page.getByRole("button", { name: /confirm/i }).click(); + + // Modal should close + await expect(confirmModalTitle).not.toBeVisible(); + + // Wait for page to refresh + await page.waitForLoadState("networkidle"); + + // If we had a count, verify it decreased + if (originalCount > 1) { + const newTotalText = page.getByText(/\d+ periods/); + const newTotalVisible = await newTotalText + .isVisible() + .catch(() => false); + if (newTotalVisible) { + const newCountMatch = (await newTotalText.textContent())?.match( + /(\d+) periods/, + ); + if (newCountMatch) { + const newCount = parseInt(newCountMatch[1], 10); + expect(newCount).toBe(originalCount - 1); + } + } + } + }); }); });