Add 5 new E2E tests for settings persistence
All checks were successful
Deploy / deploy (push) Successful in 2m28s
All checks were successful
Deploy / deploy (push) Successful in 2m28s
Tests added: - notification time changes persist after page reload - timezone changes persist after page reload - multiple settings changes persist after page reload - cycle length persistence verifies exact saved value - settings form shows correct values after save without reload Updated IMPLEMENTATION_PLAN.md with accurate E2E test counts (now 180 E2E tests). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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) + 175 E2E tests (12 files) = 1189 total tests
|
||||
**Test Coverage:** 1014 unit tests (51 files) + 180 E2E tests (12 files) = 1194 total tests
|
||||
|
||||
All P0-P5 items are complete. The project is feature complete.
|
||||
|
||||
@@ -97,16 +97,16 @@ 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, 175 tests)
|
||||
### E2E Tests (12 files, 180 tests)
|
||||
| File | Tests | Coverage |
|
||||
|------|-------|----------|
|
||||
| smoke.spec.ts | 3 | Basic app functionality |
|
||||
| auth.spec.ts | 14 | Login, protected routes |
|
||||
| dashboard.spec.ts | 24 | Dashboard display, overrides |
|
||||
| settings.spec.ts | 15 | Settings form, validation |
|
||||
| dashboard.spec.ts | 40 | Dashboard display, overrides, accessibility |
|
||||
| settings.spec.ts | 26 | Settings form, validation, persistence |
|
||||
| garmin.spec.ts | 12 | Garmin connection, expiry warnings |
|
||||
| period-logging.spec.ts | 19 | Period history, logging, modal flows |
|
||||
| calendar.spec.ts | 21 | Calendar view, ICS feed |
|
||||
| calendar.spec.ts | 30 | Calendar view, ICS feed, content validation |
|
||||
| decision-engine.spec.ts | 8 | Decision priority chain |
|
||||
| cycle.spec.ts | 11 | Cycle tracking |
|
||||
| history.spec.ts | 7 | History page |
|
||||
@@ -130,8 +130,8 @@ 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 |
|
||||
| calendar.spec.ts | +13 | ICS content validation, responsive |
|
||||
| settings.spec.ts | +6 | Persistence, timezone changes |
|
||||
| calendar.spec.ts | +4 | Responsive behavior, accessibility |
|
||||
| settings.spec.ts | +1 | Error recovery on failed save |
|
||||
| garmin.spec.ts | +4 | Token refresh, network error recovery |
|
||||
|
||||
---
|
||||
@@ -149,6 +149,7 @@ These are optional enhancements to improve E2E coverage. Not required for featur
|
||||
|
||||
## Revision History
|
||||
|
||||
- 2026-01-13: Added 5 settings persistence E2E tests (notification time, timezone, multi-field persistence)
|
||||
- 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)
|
||||
|
||||
@@ -414,5 +414,241 @@ test.describe("settings", () => {
|
||||
const isDisabledAfter = await saveButton.isDisabled();
|
||||
expect(isDisabledAfter).toBe(false);
|
||||
});
|
||||
|
||||
test("notification time changes persist after page reload", async ({
|
||||
page,
|
||||
}) => {
|
||||
const notificationTimeInput = page.getByLabel(/notification time/i);
|
||||
const isVisible = await notificationTimeInput
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
if (!isVisible) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current value
|
||||
const originalValue = await notificationTimeInput.inputValue();
|
||||
|
||||
// Set a different valid time (toggle between 08:00 and 09:00)
|
||||
const newValue = originalValue === "08:00" ? "09:00" : "08:00";
|
||||
await notificationTimeInput.fill(newValue);
|
||||
|
||||
// Save
|
||||
const saveButton = page.getByRole("button", { name: /save/i });
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check the value persisted
|
||||
const notificationTimeAfter = page.getByLabel(/notification time/i);
|
||||
const afterValue = await notificationTimeAfter.inputValue();
|
||||
|
||||
expect(afterValue).toBe(newValue);
|
||||
|
||||
// Restore original value
|
||||
await notificationTimeAfter.fill(originalValue);
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test("timezone changes persist after page reload", async ({ page }) => {
|
||||
const timezoneInput = page.getByLabel(/timezone/i);
|
||||
const isVisible = await timezoneInput.isVisible().catch(() => false);
|
||||
|
||||
if (!isVisible) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current value
|
||||
const originalValue = await timezoneInput.inputValue();
|
||||
|
||||
// Set a different timezone (toggle between two common timezones)
|
||||
const newValue =
|
||||
originalValue === "America/New_York"
|
||||
? "America/Los_Angeles"
|
||||
: "America/New_York";
|
||||
await timezoneInput.fill(newValue);
|
||||
|
||||
// Save
|
||||
const saveButton = page.getByRole("button", { name: /save/i });
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check the value persisted
|
||||
const timezoneAfter = page.getByLabel(/timezone/i);
|
||||
const afterValue = await timezoneAfter.inputValue();
|
||||
|
||||
expect(afterValue).toBe(newValue);
|
||||
|
||||
// Restore original value
|
||||
await timezoneAfter.fill(originalValue);
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test("multiple settings changes persist after page reload", async ({
|
||||
page,
|
||||
}) => {
|
||||
const cycleLengthInput = page.getByLabel(/cycle length/i);
|
||||
const notificationTimeInput = page.getByLabel(/notification time/i);
|
||||
const timezoneInput = page.getByLabel(/timezone/i);
|
||||
|
||||
const cycleLengthVisible = await cycleLengthInput
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
const notificationTimeVisible = await notificationTimeInput
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
const timezoneVisible = await timezoneInput
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
if (!cycleLengthVisible || !notificationTimeVisible || !timezoneVisible) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all original values
|
||||
const originalCycleLength = await cycleLengthInput.inputValue();
|
||||
const originalNotificationTime = await notificationTimeInput.inputValue();
|
||||
const originalTimezone = await timezoneInput.inputValue();
|
||||
|
||||
// Set different values for all fields
|
||||
const newCycleLength = originalCycleLength === "28" ? "30" : "28";
|
||||
const newNotificationTime =
|
||||
originalNotificationTime === "08:00" ? "09:00" : "08:00";
|
||||
const newTimezone =
|
||||
originalTimezone === "America/New_York"
|
||||
? "America/Los_Angeles"
|
||||
: "America/New_York";
|
||||
|
||||
await cycleLengthInput.fill(newCycleLength);
|
||||
await notificationTimeInput.fill(newNotificationTime);
|
||||
await timezoneInput.fill(newTimezone);
|
||||
|
||||
// Save all changes at once
|
||||
const saveButton = page.getByRole("button", { name: /save/i });
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Verify all values persisted
|
||||
const cycleLengthAfter = page.getByLabel(/cycle length/i);
|
||||
const notificationTimeAfter = page.getByLabel(/notification time/i);
|
||||
const timezoneAfter = page.getByLabel(/timezone/i);
|
||||
|
||||
expect(await cycleLengthAfter.inputValue()).toBe(newCycleLength);
|
||||
expect(await notificationTimeAfter.inputValue()).toBe(
|
||||
newNotificationTime,
|
||||
);
|
||||
expect(await timezoneAfter.inputValue()).toBe(newTimezone);
|
||||
|
||||
// Restore all original values
|
||||
await cycleLengthAfter.fill(originalCycleLength);
|
||||
await notificationTimeAfter.fill(originalNotificationTime);
|
||||
await timezoneAfter.fill(originalTimezone);
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test("cycle length persistence verifies exact saved value", async ({
|
||||
page,
|
||||
}) => {
|
||||
const cycleLengthInput = page.getByLabel(/cycle length/i);
|
||||
const isVisible = await cycleLengthInput.isVisible().catch(() => false);
|
||||
|
||||
if (!isVisible) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current value
|
||||
const originalValue = await cycleLengthInput.inputValue();
|
||||
|
||||
// Set a specific different valid value
|
||||
const newValue = originalValue === "28" ? "31" : "28";
|
||||
await cycleLengthInput.fill(newValue);
|
||||
|
||||
// Save
|
||||
const saveButton = page.getByRole("button", { name: /save/i });
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check the exact value persisted (not just range validation)
|
||||
const cycleLengthAfter = page.getByLabel(/cycle length/i);
|
||||
const afterValue = await cycleLengthAfter.inputValue();
|
||||
|
||||
expect(afterValue).toBe(newValue);
|
||||
|
||||
// Restore original value
|
||||
await cycleLengthAfter.fill(originalValue);
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test("settings form shows correct values after save without reload", async ({
|
||||
page,
|
||||
}) => {
|
||||
const cycleLengthInput = page.getByLabel(/cycle length/i);
|
||||
const notificationTimeInput = page.getByLabel(/notification time/i);
|
||||
|
||||
const cycleLengthVisible = await cycleLengthInput
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
const notificationTimeVisible = await notificationTimeInput
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
if (!cycleLengthVisible || !notificationTimeVisible) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get original values
|
||||
const originalCycleLength = await cycleLengthInput.inputValue();
|
||||
const originalNotificationTime = await notificationTimeInput.inputValue();
|
||||
|
||||
// Change values
|
||||
const newCycleLength = originalCycleLength === "28" ? "29" : "28";
|
||||
const newNotificationTime =
|
||||
originalNotificationTime === "08:00" ? "10:00" : "08:00";
|
||||
|
||||
await cycleLengthInput.fill(newCycleLength);
|
||||
await notificationTimeInput.fill(newNotificationTime);
|
||||
|
||||
// Save
|
||||
const saveButton = page.getByRole("button", { name: /save/i });
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Verify values are still showing the new values without reload
|
||||
expect(await cycleLengthInput.inputValue()).toBe(newCycleLength);
|
||||
expect(await notificationTimeInput.inputValue()).toBe(
|
||||
newNotificationTime,
|
||||
);
|
||||
|
||||
// Restore original values
|
||||
await cycleLengthInput.fill(originalCycleLength);
|
||||
await notificationTimeInput.fill(originalNotificationTime);
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user