From 5bfe51d630fd459052d1faa027ad43a061571831 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Tue, 13 Jan 2026 18:20:42 +0000 Subject: [PATCH] Add 5 new Garmin E2E tests for expiry warnings and lifecycle 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 --- IMPLEMENTATION_PLAN.md | 7 +- e2e/garmin.spec.ts | 181 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 3 deletions(-) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index f988e9d..d808c29 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) + 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. @@ -104,7 +104,7 @@ All P0-P5 items are complete. The project is feature complete. | auth.spec.ts | 14 | Login, protected routes | | dashboard.spec.ts | 24 | Dashboard display, overrides | | 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 | | calendar.spec.ts | 21 | Calendar view, ICS feed | | 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 | | calendar.spec.ts | +13 | ICS content validation, responsive | | 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 +- 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) - 2026-01-11: Completed P5.1-P5.4 (period history, toast, CI, E2E) diff --git a/e2e/garmin.spec.ts b/e2e/garmin.spec.ts index 88a55a2..835bd80 100644 --- a/e2e/garmin.spec.ts +++ b/e2e/garmin.spec.ts @@ -187,5 +187,186 @@ test.describe("garmin connection", () => { const expiryText = page.getByText(/\d+ days/i); 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(); + }); }); });