Add 5 new Garmin E2E tests for expiry warnings and lifecycle
All checks were successful
Deploy / deploy (push) Successful in 1m39s
All checks were successful
Deploy / deploy (push) Successful in 1m39s
New tests: - Yellow warning banner when token expires in 10 days (warning level) - Red critical banner when token expires in 5 days (critical level) - Expired token state shows token input for re-entry - Connection persists after page reload - Can reconnect after disconnecting 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) + 165 E2E tests (12 files) = 1179 total tests
|
**Test Coverage:** 1014 unit tests (51 files) + 170 E2E tests (12 files) = 1184 total tests
|
||||||
|
|
||||||
All P0-P5 items are complete. The project is feature complete.
|
All P0-P5 items are complete. The project is feature complete.
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ All P0-P5 items are complete. The project is feature complete.
|
|||||||
| auth.spec.ts | 14 | Login, protected routes |
|
| auth.spec.ts | 14 | Login, protected routes |
|
||||||
| 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 | 7 | Garmin connection |
|
| garmin.spec.ts | 12 | Garmin connection, expiry warnings |
|
||||||
| period-logging.spec.ts | 14 | Period history, logging |
|
| period-logging.spec.ts | 14 | Period history, logging |
|
||||||
| 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 |
|
||||||
@@ -133,7 +133,7 @@ These are optional enhancements to improve E2E coverage. Not required for featur
|
|||||||
| period-logging.spec.ts | +5 | Future dates, dashboard updates |
|
| 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 | +9 | Expiry warnings, token refresh |
|
| garmin.spec.ts | +4 | Token refresh, network error recovery |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -150,6 +150,7 @@ These are optional enhancements to improve E2E coverage. Not required for featur
|
|||||||
|
|
||||||
## Revision History
|
## Revision History
|
||||||
|
|
||||||
|
- 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)
|
||||||
- 2026-01-11: Completed P5.1-P5.4 (period history, toast, CI, E2E)
|
- 2026-01-11: Completed P5.1-P5.4 (period history, toast, CI, E2E)
|
||||||
|
|||||||
@@ -187,5 +187,186 @@ test.describe("garmin connection", () => {
|
|||||||
const expiryText = page.getByText(/\d+ days/i);
|
const expiryText = page.getByText(/\d+ days/i);
|
||||||
await expect(expiryText).toBeVisible();
|
await expect(expiryText).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("shows yellow warning banner when token expires in 10 days", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// Connect with tokens expiring in 10 days (warning level: 8-14 days)
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + 10);
|
||||||
|
|
||||||
|
const validTokens = JSON.stringify({
|
||||||
|
oauth1: { token: "test-token-warning", secret: "test-secret" },
|
||||||
|
oauth2: { access_token: "test-access-token" },
|
||||||
|
expires_at: expiresAt.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenInput = page.locator("#tokenInput");
|
||||||
|
await tokenInput.fill(validTokens);
|
||||||
|
await page.getByRole("button", { name: /save tokens/i }).click();
|
||||||
|
|
||||||
|
// Wait for connected state
|
||||||
|
await expect(page.getByText("Connected", { exact: true })).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should show warning banner with yellow styling
|
||||||
|
const warningBanner = page.getByTestId("expiry-warning");
|
||||||
|
await expect(warningBanner).toBeVisible();
|
||||||
|
await expect(warningBanner).toContainText("Token expiring soon");
|
||||||
|
|
||||||
|
// Verify yellow warning styling (not red critical)
|
||||||
|
await expect(warningBanner).toHaveClass(/bg-yellow/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows red critical banner when token expires in 5 days", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// Connect with tokens expiring in 5 days (critical level: <= 7 days)
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + 5);
|
||||||
|
|
||||||
|
const validTokens = JSON.stringify({
|
||||||
|
oauth1: { token: "test-token-critical", secret: "test-secret" },
|
||||||
|
oauth2: { access_token: "test-access-token" },
|
||||||
|
expires_at: expiresAt.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenInput = page.locator("#tokenInput");
|
||||||
|
await tokenInput.fill(validTokens);
|
||||||
|
await page.getByRole("button", { name: /save tokens/i }).click();
|
||||||
|
|
||||||
|
// Wait for connected state
|
||||||
|
await expect(page.getByText("Connected", { exact: true })).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should show critical banner with red styling
|
||||||
|
const warningBanner = page.getByTestId("expiry-warning");
|
||||||
|
await expect(warningBanner).toBeVisible();
|
||||||
|
await expect(warningBanner).toContainText("Token expires soon!");
|
||||||
|
|
||||||
|
// Verify red critical styling
|
||||||
|
await expect(warningBanner).toHaveClass(/bg-red/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows expired state with token input when tokens have expired", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// Connect with tokens that expire yesterday (already expired)
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setDate(expiresAt.getDate() - 1);
|
||||||
|
|
||||||
|
const validTokens = JSON.stringify({
|
||||||
|
oauth1: { token: "test-token-expired", secret: "test-secret" },
|
||||||
|
oauth2: { access_token: "test-access-token" },
|
||||||
|
expires_at: expiresAt.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenInput = page.locator("#tokenInput");
|
||||||
|
await tokenInput.fill(validTokens);
|
||||||
|
await page.getByRole("button", { name: /save tokens/i }).click();
|
||||||
|
|
||||||
|
// Wait for save to complete
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Should show expired state
|
||||||
|
const expiredText = page.getByText("Token Expired");
|
||||||
|
await expect(expiredText).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
// Token input should be visible to allow re-entry
|
||||||
|
await expect(page.locator("#tokenInput")).toBeVisible();
|
||||||
|
|
||||||
|
// Red indicator should be visible
|
||||||
|
const redIndicator = page.locator(".bg-red-500").first();
|
||||||
|
await expect(redIndicator).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connection persists after page reload", async ({ page }) => {
|
||||||
|
// First connect with valid tokens
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + 60);
|
||||||
|
|
||||||
|
const validTokens = JSON.stringify({
|
||||||
|
oauth1: { token: "test-token-persist", secret: "test-secret" },
|
||||||
|
oauth2: { access_token: "test-access-token" },
|
||||||
|
expires_at: expiresAt.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenInput = page.locator("#tokenInput");
|
||||||
|
await tokenInput.fill(validTokens);
|
||||||
|
await page.getByRole("button", { name: /save tokens/i }).click();
|
||||||
|
|
||||||
|
// Wait for connected state
|
||||||
|
await expect(page.getByText("Connected", { exact: true })).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload the page
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// Should still show connected state
|
||||||
|
await expect(page.getByText("Connected", { exact: true })).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Green indicator should still be visible
|
||||||
|
const greenIndicator = page.locator(".bg-green-500").first();
|
||||||
|
await expect(greenIndicator).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can reconnect after disconnecting", async ({ page }) => {
|
||||||
|
// First connect
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + 30);
|
||||||
|
|
||||||
|
const validTokens = JSON.stringify({
|
||||||
|
oauth1: { token: "test-token-reconnect", secret: "test-secret" },
|
||||||
|
oauth2: { access_token: "test-access-token" },
|
||||||
|
expires_at: expiresAt.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenInput = page.locator("#tokenInput");
|
||||||
|
await tokenInput.fill(validTokens);
|
||||||
|
await page.getByRole("button", { name: /save tokens/i }).click();
|
||||||
|
|
||||||
|
// Wait for connected state
|
||||||
|
await expect(page.getByText("Connected", { exact: true })).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disconnect
|
||||||
|
await page.getByRole("button", { name: /disconnect/i }).click();
|
||||||
|
await expect(page.getByText(/not connected/i)).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reconnect with new tokens
|
||||||
|
const newExpiresAt = new Date();
|
||||||
|
newExpiresAt.setDate(newExpiresAt.getDate() + 90);
|
||||||
|
|
||||||
|
const newTokens = JSON.stringify({
|
||||||
|
oauth1: {
|
||||||
|
token: "test-token-reconnect-new",
|
||||||
|
secret: "test-secret-new",
|
||||||
|
},
|
||||||
|
oauth2: { access_token: "test-access-token-new" },
|
||||||
|
expires_at: newExpiresAt.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const newTokenInput = page.locator("#tokenInput");
|
||||||
|
await newTokenInput.fill(newTokens);
|
||||||
|
await page.getByRole("button", { name: /save tokens/i }).click();
|
||||||
|
|
||||||
|
// Wait for reconnection
|
||||||
|
await expect(page.getByText("Connected", { exact: true })).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Green indicator should be visible
|
||||||
|
const greenIndicator = page.locator(".bg-green-500").first();
|
||||||
|
await expect(greenIndicator).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user