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
|
## 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.
|
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 |
|
| PeriodDateModal | 22 | Period input modal |
|
||||||
| Skeletons | 29 | Loading states with shimmer |
|
| Skeletons | 29 | Loading states with shimmer |
|
||||||
|
|
||||||
### E2E Tests (12 files, 175 tests)
|
### E2E Tests (12 files, 180 tests)
|
||||||
| File | Tests | Coverage |
|
| File | Tests | Coverage |
|
||||||
|------|-------|----------|
|
|------|-------|----------|
|
||||||
| smoke.spec.ts | 3 | Basic app functionality |
|
| smoke.spec.ts | 3 | Basic app functionality |
|
||||||
| auth.spec.ts | 14 | Login, protected routes |
|
| auth.spec.ts | 14 | Login, protected routes |
|
||||||
| dashboard.spec.ts | 24 | Dashboard display, overrides |
|
| dashboard.spec.ts | 40 | Dashboard display, overrides, accessibility |
|
||||||
| settings.spec.ts | 15 | Settings form, validation |
|
| settings.spec.ts | 26 | Settings form, validation, persistence |
|
||||||
| garmin.spec.ts | 12 | Garmin connection, expiry warnings |
|
| garmin.spec.ts | 12 | Garmin connection, expiry warnings |
|
||||||
| period-logging.spec.ts | 19 | Period history, logging, modal flows |
|
| 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 |
|
| decision-engine.spec.ts | 8 | Decision priority chain |
|
||||||
| cycle.spec.ts | 11 | Cycle tracking |
|
| cycle.spec.ts | 11 | Cycle tracking |
|
||||||
| history.spec.ts | 7 | History page |
|
| 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 |
|
| File | Additional Tests | Focus Area |
|
||||||
|------|------------------|------------|
|
|------|------------------|------------|
|
||||||
| auth.spec.ts | +6 | OIDC flow, session persistence |
|
| auth.spec.ts | +6 | OIDC flow, session persistence |
|
||||||
| calendar.spec.ts | +13 | ICS content validation, responsive |
|
| calendar.spec.ts | +4 | Responsive behavior, accessibility |
|
||||||
| settings.spec.ts | +6 | Persistence, timezone changes |
|
| settings.spec.ts | +1 | Error recovery on failed save |
|
||||||
| garmin.spec.ts | +4 | Token refresh, network error recovery |
|
| 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
|
## 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 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)
|
||||||
|
|||||||
@@ -414,5 +414,241 @@ test.describe("settings", () => {
|
|||||||
const isDisabledAfter = await saveButton.isDisabled();
|
const isDisabledAfter = await saveButton.isDisabled();
|
||||||
expect(isDisabledAfter).toBe(false);
|
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