diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index c66ee53..6073121 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) + 202 E2E tests (13 files) = 1216 total tests +**Test Coverage:** 1014 unit tests (51 files) + 204 E2E tests (14 files) = 1218 total tests 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 | | Skeletons | 29 | Loading states with shimmer | -### E2E Tests (13 files, 202 tests) +### E2E Tests (14 files, 204 tests) | File | Tests | Coverage | |------|-------|----------| | smoke.spec.ts | 3 | Basic app functionality | @@ -113,6 +113,7 @@ All P0-P5 items are complete. The project is feature complete. | plan.spec.ts | 7 | Plan page | | health.spec.ts | 3 | Health/observability | | mobile.spec.ts | 7 | Mobile viewport behavior, responsive layout, calendar mobile | +| dark-mode.spec.ts | 2 | System preference detection, light/dark theme application | --- @@ -124,11 +125,11 @@ These are optional enhancements to improve E2E coverage. Not required for featur | File | Tests | Description | |------|-------|-------------| | notifications.spec.ts | 3 | Notification preferences | -| dark-mode.spec.ts | 2 | System preference detection | ### Completed Enhancements | File | Tests Added | Focus Area | |------|-------------|------------| +| dark-mode.spec.ts | +2 | System preference detection (light/dark mode) | | garmin.spec.ts | +4 | Network error recovery (save, disconnect, status fetch, retry) | | calendar.spec.ts | +4 | Accessibility (ARIA, keyboard nav) | | settings.spec.ts | +1 | Error recovery on failed save | @@ -149,6 +150,7 @@ These are optional enhancements to improve E2E coverage. Not required for featur ## Revision History +- 2026-01-13: Added dark-mode.spec.ts with 2 E2E tests (system preference detection for light/dark mode) - 2026-01-13: Added 4 Garmin E2E tests (network error recovery for save, disconnect, status fetch, retry) - 2026-01-13: Added 8 E2E tests (calendar accessibility, settings error recovery, calendar mobile behavior) - 2026-01-13: Added mobile.spec.ts with 4 E2E tests (mobile viewport behavior, responsive layout) diff --git a/e2e/dark-mode.spec.ts b/e2e/dark-mode.spec.ts new file mode 100644 index 0000000..b0147ad --- /dev/null +++ b/e2e/dark-mode.spec.ts @@ -0,0 +1,90 @@ +// ABOUTME: E2E tests for dark mode system preference detection. +// ABOUTME: Verifies app respects OS-level dark mode setting via prefers-color-scheme. +import { expect, test } from "@playwright/test"; + +// Helper to parse background color and determine if it's light or dark +function isLightBackground(colorValue: string): boolean { + // Try rgb/rgba format: rgb(255, 255, 255) or rgba(255, 255, 255, 1) + const rgbMatch = colorValue.match( + /rgba?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/, + ); + + if (rgbMatch) { + const r = parseFloat(rgbMatch[1]); + const g = parseFloat(rgbMatch[2]); + const b = parseFloat(rgbMatch[3]); + // Calculate relative luminance (simplified) + const luminance = (r + g + b) / 3; + return luminance > 128; + } + + // Try oklch format: oklch(1 0 0) where first value is lightness + const oklchMatch = colorValue.match(/oklch\s*\(\s*([\d.]+)/); + if (oklchMatch) { + const lightness = parseFloat(oklchMatch[1]); + return lightness > 0.5; + } + + // Try color() format with oklch: color(oklch 1 0 0) + const colorOklchMatch = colorValue.match(/color\s*\(\s*oklch\s+([\d.]+)/); + if (colorOklchMatch) { + const lightness = parseFloat(colorOklchMatch[1]); + return lightness > 0.5; + } + + // Try lab() format: lab(100 0 0) where first value is lightness 0-100 + const labMatch = colorValue.match(/lab\s*\(\s*([\d.]+)/); + if (labMatch) { + const lightness = parseFloat(labMatch[1]); + return lightness > 50; + } + + // Default: couldn't parse, assume we need to fail the test + return false; +} + +test.describe("dark mode", () => { + test("applies light mode styling when system prefers light", async ({ + page, + }) => { + // Set system preference to light mode + await page.emulateMedia({ colorScheme: "light" }); + + await page.goto("/login"); + await page.waitForLoadState("domcontentloaded"); + + // Get computed background color of the body + const bodyBgColor = await page.evaluate(() => { + return window.getComputedStyle(document.body).backgroundColor; + }); + + // In light mode, background should be white/light (oklch(1 0 0) = white) + const isLight = isLightBackground(bodyBgColor); + expect( + isLight, + `Expected light background in light mode, got: ${bodyBgColor}`, + ).toBe(true); + }); + + test("applies dark mode styling when system prefers dark", async ({ + page, + }) => { + // Set system preference to dark mode + await page.emulateMedia({ colorScheme: "dark" }); + + await page.goto("/login"); + await page.waitForLoadState("domcontentloaded"); + + // Get computed background color of the body + const bodyBgColor = await page.evaluate(() => { + return window.getComputedStyle(document.body).backgroundColor; + }); + + // In dark mode, background should be dark (oklch(0.145 0 0) = near black) + const isLight = isLightBackground(bodyBgColor); + expect( + isLight, + `Expected dark background in dark mode, got: ${bodyBgColor}`, + ).toBe(false); + }); +});