Add 5 new E2E tests for period logging modal and edit/delete flows
All checks were successful
Deploy / deploy (push) Successful in 2m38s

New tests cover:
- Period date modal opens from dashboard onboarding banner
- Period date input restricts future dates via max attribute
- Logging period from modal updates dashboard cycle info
- Edit period modal flow changes date successfully
- Delete period confirmation flow removes entry

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-13 18:26:03 +00:00
parent 5bfe51d630
commit 79414b813a
2 changed files with 263 additions and 4 deletions

View File

@@ -4,7 +4,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
## Current Status: Feature Complete ## 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. 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 | | PeriodDateModal | 22 | Period input modal |
| Skeletons | 29 | Loading states with shimmer | | Skeletons | 29 | Loading states with shimmer |
### E2E Tests (12 files, 165 tests) ### E2E Tests (12 files, 175 tests)
| File | Tests | Coverage | | File | Tests | Coverage |
|------|-------|----------| |------|-------|----------|
| smoke.spec.ts | 3 | Basic app functionality | | 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 | | dashboard.spec.ts | 24 | Dashboard display, overrides |
| settings.spec.ts | 15 | Settings form, validation | | settings.spec.ts | 15 | Settings form, validation |
| garmin.spec.ts | 12 | Garmin connection, expiry warnings | | 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 | | calendar.spec.ts | 21 | Calendar view, ICS feed |
| decision-engine.spec.ts | 8 | Decision priority chain | | decision-engine.spec.ts | 8 | Decision priority chain |
| cycle.spec.ts | 11 | Cycle tracking | | 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 | | File | Additional Tests | Focus Area |
|------|------------------|------------| |------|------------------|------------|
| auth.spec.ts | +6 | OIDC flow, session persistence | | 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 | | calendar.spec.ts | +13 | ICS content validation, responsive |
| settings.spec.ts | +6 | Persistence, timezone changes | | settings.spec.ts | +6 | Persistence, timezone changes |
| garmin.spec.ts | +4 | Token refresh, network error recovery | | 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 ## 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: 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-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) - 2026-01-12: Fixed spec gaps (email format, HRV colors, progress bar, emojis)

View File

@@ -285,5 +285,264 @@ test.describe("period logging", () => {
await expect(editButton.first()).toBeVisible(); 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);
}
}
}
});
}); });
}); });