From 267d45f98a29f86fdb34879b47b84132daacaba8 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Sun, 11 Jan 2026 09:02:34 +0000 Subject: [PATCH] Add component tests for P3.11 (82 tests across 5 files) - DecisionCard tests: 11 tests covering rendering, status icons, styling - DataPanel tests: 18 tests covering biometrics display, null handling, styling - NutritionPanel tests: 12 tests covering seeds, carbs, keto guidance display - OverrideToggles tests: 18 tests covering toggle states, callbacks, styling - DayCell tests: 23 tests covering phase coloring, today highlighting, click handling Total tests now: 707 passing across 40 test files Co-Authored-By: Claude Opus 4.5 --- IMPLEMENTATION_PLAN.md | 46 ++-- src/components/calendar/day-cell.test.tsx | 190 +++++++++++++++ src/components/dashboard/data-panel.test.tsx | 143 ++++++++++++ .../dashboard/decision-card.test.tsx | 166 ++++++++++++++ .../dashboard/nutrition-panel.test.tsx | 136 +++++++++++ .../dashboard/override-toggles.test.tsx | 216 ++++++++++++++++++ 6 files changed, 874 insertions(+), 23 deletions(-) create mode 100644 src/components/calendar/day-cell.test.tsx create mode 100644 src/components/dashboard/data-panel.test.tsx create mode 100644 src/components/dashboard/decision-card.test.tsx create mode 100644 src/components/dashboard/nutrition-panel.test.tsx create mode 100644 src/components/dashboard/override-toggles.test.tsx diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 177622b..5e8805b 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 State Summary -### Overall Status: 625 tests passing across 35 test files +### Overall Status: 707 tests passing across 40 test files ### Library Implementation | File | Status | Gap Analysis | @@ -110,12 +110,12 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta | `src/app/calendar/page.test.tsx` | **EXISTS** - 23 tests (rendering, navigation, ICS subscription, token regeneration) | | `src/app/settings/page.test.tsx` | **EXISTS** - 24+ tests (form rendering, validation, submission) | | `src/app/settings/garmin/page.test.tsx` | **EXISTS** - 27 tests (connection status, token management) | -| `src/components/dashboard/decision-card.test.tsx` | **MISSING** - Needs tests for rendering, status icons | -| `src/components/dashboard/data-panel.test.tsx` | **MISSING** - Needs tests for biometrics display | -| `src/components/dashboard/nutrition-panel.test.tsx` | **MISSING** - Needs tests for nutrition guidance display | -| `src/components/dashboard/override-toggles.test.tsx` | **MISSING** - Needs tests for toggle state and callbacks | +| `src/components/dashboard/decision-card.test.tsx` | **EXISTS** - 11 tests (rendering, status icons, styling) | +| `src/components/dashboard/data-panel.test.tsx` | **EXISTS** - 18 tests (biometrics display, null handling, styling) | +| `src/components/dashboard/nutrition-panel.test.tsx` | **EXISTS** - 12 tests (seeds, carbs, keto guidance) | +| `src/components/dashboard/override-toggles.test.tsx` | **EXISTS** - 18 tests (toggle states, callbacks, styling) | | `src/components/dashboard/mini-calendar.test.tsx` | **EXISTS** - 23 tests (calendar grid, phase colors, navigation, legend) | -| `src/components/calendar/day-cell.test.tsx` | **MISSING** - Needs tests for phase coloring, click handler | +| `src/components/calendar/day-cell.test.tsx` | **EXISTS** - 23 tests (phase coloring, today highlighting, click handling) | | `src/app/plan/page.test.tsx` | **EXISTS** - 16 tests (loading states, error handling, phase display, exercise reference, rebounding techniques) | | E2E tests | **AUTHORIZED SKIP** - Per specs/testing.md | @@ -665,20 +665,21 @@ Testing, error handling, and refinements. - **Why:** Confidence in production deployment - **Status:** Per specs/testing.md: "End-to-end tests are not required for MVP (authorized skip)" -### P3.11: Missing Component Tests -- [ ] Add unit tests for untested components -- **Components Needing Tests (5 total):** - - `src/components/dashboard/decision-card.tsx` - Tests for rendering decision status, icon, and reason - - `src/components/dashboard/data-panel.tsx` - Tests for biometrics display (BB, HRV, intensity) - - `src/components/dashboard/nutrition-panel.tsx` - Tests for seeds, carbs, keto guidance display - - `src/components/dashboard/override-toggles.tsx` - Tests for toggle states and callbacks (has interactive state) - - `src/components/calendar/day-cell.tsx` - Tests for phase coloring and click handler -- **Test Files to Create:** - - `src/components/dashboard/decision-card.test.tsx` - - `src/components/dashboard/data-panel.test.tsx` - - `src/components/dashboard/nutrition-panel.test.tsx` - - `src/components/dashboard/override-toggles.test.tsx` - - `src/components/calendar/day-cell.test.tsx` +### P3.11: Missing Component Tests ✅ COMPLETE +- [x] Add unit tests for untested components +- **Components Tested (5 total):** + - `src/components/dashboard/decision-card.tsx` - 11 tests for rendering decision status, icon, reason, styling + - `src/components/dashboard/data-panel.tsx` - 18 tests for biometrics display (BB, HRV, intensity), null handling, styling + - `src/components/dashboard/nutrition-panel.tsx` - 12 tests for seeds, carbs, keto guidance display + - `src/components/dashboard/override-toggles.tsx` - 18 tests for toggle states, callbacks, styling + - `src/components/calendar/day-cell.tsx` - 23 tests for phase coloring, today highlighting, click handling +- **Test Files Created:** + - `src/components/dashboard/decision-card.test.tsx` - 11 tests + - `src/components/dashboard/data-panel.test.tsx` - 18 tests + - `src/components/dashboard/nutrition-panel.test.tsx` - 12 tests + - `src/components/dashboard/override-toggles.test.tsx` - 18 tests + - `src/components/calendar/day-cell.test.tsx` - 23 tests +- **Total Tests Added:** 82 tests across 5 files - **Why:** Component isolation ensures UI correctness and prevents regressions --- @@ -797,7 +798,6 @@ P4.* UX Polish ────────> After core functionality complete | Priority | Task | Effort | Notes | |----------|------|--------|-------| | Medium | P2.18 OIDC Auth | Large | Production auth requirement | -| Medium | P3.11 Component Tests | Medium | 5 components need tests | | Low | P3.7 Error Handling | Small | Polish | | Low | P3.8 Loading States | Small | Polish | | Low | P4.* UX Polish | Various | After core complete | @@ -812,7 +812,6 @@ P4.* UX Polish ────────> After core functionality complete | P0.4 | P0.1, P0.2 | P1.7, P2.9, P2.10, P2.13 | | P2.18 | P1.6 | - | | P3.9 | P2.4 | - | -| P3.11 | - | - | --- @@ -880,6 +879,7 @@ P4.* UX Polish ────────> After core functionality complete - [x] **P3.5: Encryption Tests** - Complete with 14 tests covering AES-256-GCM round-trip, error handling, key validation - [x] **P3.6: Garmin Tests** - Complete with 33 tests covering API interactions, token expiry, error handling - [x] **P3.9: Token Expiration Warnings** - Complete with 10 new tests in email.test.ts, 10 new tests in garmin-sync/route.test.ts; sends warnings at 14 and 7 days before expiry +- [x] **P3.11: Missing Component Tests** - Complete with 82 tests across 5 component test files (DecisionCard: 11, DataPanel: 18, NutritionPanel: 12, OverrideToggles: 18, DayCell: 23) --- @@ -912,4 +912,4 @@ P4.* UX Polish ────────> After core functionality complete 13. **OIDC vs Email/Password:** Current email/password login (P1.6) works for development. P2.18 upgrades to OIDC for production security per specs/authentication.md 14. **E2E Tests:** Authorized skip per specs/testing.md - unit and integration tests are sufficient for MVP 15. **Dark Mode:** Partial Tailwind support exists via dark: classes but may need prefers-color-scheme configuration in tailwind.config.js (see P4.3) -16. **Component Tests:** 5 components lack unit tests (P3.11) - DecisionCard, DataPanel, NutritionPanel, OverrideToggles, DayCell +16. **Component Tests:** P3.11 COMPLETE - All 5 dashboard and calendar components now have comprehensive unit tests (82 tests total) diff --git a/src/components/calendar/day-cell.test.tsx b/src/components/calendar/day-cell.test.tsx new file mode 100644 index 0000000..36e1f40 --- /dev/null +++ b/src/components/calendar/day-cell.test.tsx @@ -0,0 +1,190 @@ +// ABOUTME: Unit tests for DayCell component. +// ABOUTME: Tests phase coloring, today highlighting, and click handling. +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { CyclePhase } from "@/types"; +import { DayCell } from "./day-cell"; + +describe("DayCell", () => { + const baseProps = { + date: new Date("2026-01-15"), + cycleDay: 5, + phase: "FOLLICULAR" as CyclePhase, + isToday: false, + onClick: vi.fn(), + }; + + describe("rendering", () => { + it("renders the day number from date", () => { + render(); + + expect(screen.getByText("15")).toBeInTheDocument(); + }); + + it("renders the cycle day label", () => { + render(); + + expect(screen.getByText("Day 5")).toBeInTheDocument(); + }); + + it("renders as a button", () => { + render(); + + expect(screen.getByRole("button")).toBeInTheDocument(); + }); + + it("renders cycle day 1 correctly", () => { + render(); + + expect(screen.getByText("Day 1")).toBeInTheDocument(); + }); + + it("renders high cycle day numbers", () => { + render(); + + expect(screen.getByText("Day 28")).toBeInTheDocument(); + }); + }); + + describe("phase colors", () => { + it("applies blue background for MENSTRUAL phase", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-blue-100"); + }); + + it("applies green background for FOLLICULAR phase", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-green-100"); + }); + + it("applies purple background for OVULATION phase", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-purple-100"); + }); + + it("applies yellow background for EARLY_LUTEAL phase", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-yellow-100"); + }); + + it("applies red background for LATE_LUTEAL phase", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-red-100"); + }); + }); + + describe("today highlighting", () => { + it("does not have ring when isToday is false", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).not.toHaveClass("ring-2", "ring-black"); + }); + + it("has ring-2 ring-black when isToday is true", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("ring-2", "ring-black"); + }); + + it("maintains phase color when isToday is true", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-purple-100"); + expect(button).toHaveClass("ring-2", "ring-black"); + }); + }); + + describe("click handling", () => { + it("calls onClick when clicked", () => { + const onClick = vi.fn(); + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it("does not throw when onClick is undefined", () => { + render(); + + const button = screen.getByRole("button"); + expect(() => fireEvent.click(button)).not.toThrow(); + }); + + it("calls onClick once per click", () => { + const onClick = vi.fn(); + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + fireEvent.click(button); + fireEvent.click(button); + + expect(onClick).toHaveBeenCalledTimes(3); + }); + }); + + describe("styling", () => { + it("has rounded corners", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("rounded"); + }); + + it("has padding", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("p-2"); + }); + + it("renders day number with font-medium", () => { + render(); + + const dayNumber = screen.getByText("15"); + expect(dayNumber).toHaveClass("font-medium"); + }); + + it("renders cycle day label in gray", () => { + render(); + + const cycleLabel = screen.getByText("Day 5"); + expect(cycleLabel).toHaveClass("text-gray-500"); + }); + }); + + describe("date variations", () => { + it("renders single digit day", () => { + render(); + + expect(screen.getByText("5")).toBeInTheDocument(); + }); + + it("renders last day of month", () => { + render(); + + expect(screen.getByText("31")).toBeInTheDocument(); + }); + + it("renders first day of month", () => { + render(); + + expect(screen.getByText("1")).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/dashboard/data-panel.test.tsx b/src/components/dashboard/data-panel.test.tsx new file mode 100644 index 0000000..747c9c4 --- /dev/null +++ b/src/components/dashboard/data-panel.test.tsx @@ -0,0 +1,143 @@ +// ABOUTME: Unit tests for DataPanel component. +// ABOUTME: Tests biometrics display including body battery, HRV, and intensity. +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { DataPanel } from "./data-panel"; + +describe("DataPanel", () => { + const baseProps = { + bodyBatteryCurrent: 75, + bodyBatteryYesterdayLow: 25, + hrvStatus: "Balanced", + weekIntensity: 120, + phaseLimit: 200, + remainingMinutes: 80, + }; + + describe("rendering", () => { + it("renders the YOUR DATA heading", () => { + render(); + + expect(screen.getByText("YOUR DATA")).toBeInTheDocument(); + }); + + it("renders body battery current value", () => { + render(); + + expect(screen.getByText(/Body Battery: 75/)).toBeInTheDocument(); + }); + + it("renders yesterday low value", () => { + render(); + + expect(screen.getByText(/Yesterday Low: 25/)).toBeInTheDocument(); + }); + + it("renders HRV status", () => { + render(); + + expect(screen.getByText(/HRV: Balanced/)).toBeInTheDocument(); + }); + + it("renders week intensity with phase limit", () => { + render(); + + expect(screen.getByText(/Week: 120\/200 min/)).toBeInTheDocument(); + }); + + it("renders remaining minutes", () => { + render(); + + expect(screen.getByText(/Remaining: 80 min/)).toBeInTheDocument(); + }); + }); + + describe("null value handling", () => { + it("displays N/A when bodyBatteryCurrent is null", () => { + render(); + + expect(screen.getByText(/Body Battery: N\/A/)).toBeInTheDocument(); + }); + + it("displays N/A when bodyBatteryYesterdayLow is null", () => { + render(); + + expect(screen.getByText(/Yesterday Low: N\/A/)).toBeInTheDocument(); + }); + + it("displays N/A for both when both are null", () => { + render( + , + ); + + expect(screen.getByText(/Body Battery: N\/A/)).toBeInTheDocument(); + expect(screen.getByText(/Yesterday Low: N\/A/)).toBeInTheDocument(); + }); + }); + + describe("HRV status variations", () => { + it("displays Balanced HRV status", () => { + render(); + + expect(screen.getByText(/HRV: Balanced/)).toBeInTheDocument(); + }); + + it("displays Unbalanced HRV status", () => { + render(); + + expect(screen.getByText(/HRV: Unbalanced/)).toBeInTheDocument(); + }); + + it("displays Unknown HRV status", () => { + render(); + + expect(screen.getByText(/HRV: Unknown/)).toBeInTheDocument(); + }); + }); + + describe("intensity values", () => { + it("displays zero intensity correctly", () => { + render(); + + expect(screen.getByText(/Week: 0\/200 min/)).toBeInTheDocument(); + }); + + it("displays when over phase limit", () => { + render(); + + expect(screen.getByText(/Week: 250\/200 min/)).toBeInTheDocument(); + }); + + it("displays zero remaining minutes", () => { + render(); + + expect(screen.getByText(/Remaining: 0 min/)).toBeInTheDocument(); + }); + + it("displays negative remaining minutes", () => { + render(); + + expect(screen.getByText(/Remaining: -50 min/)).toBeInTheDocument(); + }); + }); + + describe("styling", () => { + it("renders within a bordered container", () => { + const { container } = render(); + + const panel = container.firstChild as HTMLElement; + expect(panel).toHaveClass("rounded-lg", "border", "p-4"); + }); + + it("renders heading with semibold font", () => { + render(); + + const heading = screen.getByText("YOUR DATA"); + expect(heading).toHaveClass("font-semibold"); + }); + }); +}); diff --git a/src/components/dashboard/decision-card.test.tsx b/src/components/dashboard/decision-card.test.tsx new file mode 100644 index 0000000..b32a45e --- /dev/null +++ b/src/components/dashboard/decision-card.test.tsx @@ -0,0 +1,166 @@ +// ABOUTME: Unit tests for DecisionCard component. +// ABOUTME: Tests rendering of decision status, icon, and reason. +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import type { Decision } from "@/types"; +import { DecisionCard } from "./decision-card"; + +describe("DecisionCard", () => { + describe("rendering", () => { + it("renders the decision icon", () => { + const decision: Decision = { + status: "REST", + reason: "Your body needs recovery today", + icon: "🛌", + }; + + render(); + + expect(screen.getByText("🛌")).toBeInTheDocument(); + }); + + it("renders the decision status", () => { + const decision: Decision = { + status: "TRAIN", + reason: "Good to go", + icon: "🏃", + }; + + render(); + + expect(screen.getByText("TRAIN")).toBeInTheDocument(); + }); + + it("renders the decision reason", () => { + const decision: Decision = { + status: "GENTLE", + reason: "Take it easy today", + icon: "🧘", + }; + + render(); + + expect(screen.getByText("Take it easy today")).toBeInTheDocument(); + }); + }); + + describe("different status types", () => { + it("renders REST status correctly", () => { + const decision: Decision = { + status: "REST", + reason: "HRV unbalanced - recovery day", + icon: "🛌", + }; + + render(); + + expect(screen.getByText("REST")).toBeInTheDocument(); + expect(screen.getByText("🛌")).toBeInTheDocument(); + expect( + screen.getByText("HRV unbalanced - recovery day"), + ).toBeInTheDocument(); + }); + + it("renders GENTLE status correctly", () => { + const decision: Decision = { + status: "GENTLE", + reason: "Light movement recommended", + icon: "🧘", + }; + + render(); + + expect(screen.getByText("GENTLE")).toBeInTheDocument(); + expect(screen.getByText("🧘")).toBeInTheDocument(); + expect( + screen.getByText("Light movement recommended"), + ).toBeInTheDocument(); + }); + + it("renders LIGHT status correctly", () => { + const decision: Decision = { + status: "LIGHT", + reason: "Keep intensity moderate", + icon: "🚶", + }; + + render(); + + expect(screen.getByText("LIGHT")).toBeInTheDocument(); + expect(screen.getByText("🚶")).toBeInTheDocument(); + expect(screen.getByText("Keep intensity moderate")).toBeInTheDocument(); + }); + + it("renders REDUCED status correctly", () => { + const decision: Decision = { + status: "REDUCED", + reason: "Lower intensity today", + icon: "⬇️", + }; + + render(); + + expect(screen.getByText("REDUCED")).toBeInTheDocument(); + expect(screen.getByText("⬇️")).toBeInTheDocument(); + expect(screen.getByText("Lower intensity today")).toBeInTheDocument(); + }); + + it("renders TRAIN status correctly", () => { + const decision: Decision = { + status: "TRAIN", + reason: "Full intensity training approved", + icon: "🏃", + }; + + render(); + + expect(screen.getByText("TRAIN")).toBeInTheDocument(); + expect(screen.getByText("🏃")).toBeInTheDocument(); + expect( + screen.getByText("Full intensity training approved"), + ).toBeInTheDocument(); + }); + }); + + describe("styling", () => { + it("renders within a bordered container", () => { + const decision: Decision = { + status: "REST", + reason: "Test reason", + icon: "🛌", + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass("rounded-lg", "border", "p-6"); + }); + + it("renders status as bold heading", () => { + const decision: Decision = { + status: "TRAIN", + reason: "Test", + icon: "🏃", + }; + + render(); + + const heading = screen.getByRole("heading", { level: 2 }); + expect(heading).toHaveTextContent("TRAIN"); + expect(heading).toHaveClass("font-bold"); + }); + + it("renders reason with muted color", () => { + const decision: Decision = { + status: "REST", + reason: "Muted reason text", + icon: "🛌", + }; + + render(); + + const reason = screen.getByText("Muted reason text"); + expect(reason).toHaveClass("text-gray-600"); + }); + }); +}); diff --git a/src/components/dashboard/nutrition-panel.test.tsx b/src/components/dashboard/nutrition-panel.test.tsx new file mode 100644 index 0000000..2f09987 --- /dev/null +++ b/src/components/dashboard/nutrition-panel.test.tsx @@ -0,0 +1,136 @@ +// ABOUTME: Unit tests for NutritionPanel component. +// ABOUTME: Tests display of seeds, carb range, and keto guidance. +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import type { NutritionGuidance } from "@/types"; +import { NutritionPanel } from "./nutrition-panel"; + +describe("NutritionPanel", () => { + const baseNutrition: NutritionGuidance = { + seeds: "Flax & Pumpkin", + carbRange: "100-150g", + ketoGuidance: "Moderate carbs today", + }; + + describe("rendering", () => { + it("renders the NUTRITION TODAY heading", () => { + render(); + + expect(screen.getByText("NUTRITION TODAY")).toBeInTheDocument(); + }); + + it("renders seeds guidance with emoji", () => { + render(); + + expect(screen.getByText(/🌱 Flax & Pumpkin/)).toBeInTheDocument(); + }); + + it("renders carb range with emoji", () => { + render(); + + expect(screen.getByText(/🍽️ Carbs: 100-150g/)).toBeInTheDocument(); + }); + + it("renders keto guidance with emoji", () => { + render(); + + expect( + screen.getByText(/🥑 Keto: Moderate carbs today/), + ).toBeInTheDocument(); + }); + }); + + describe("seed cycling phases", () => { + it("displays follicular phase seeds (flax & pumpkin)", () => { + const nutrition: NutritionGuidance = { + ...baseNutrition, + seeds: "Flax & Pumpkin", + }; + + render(); + + expect(screen.getByText(/🌱 Flax & Pumpkin/)).toBeInTheDocument(); + }); + + it("displays luteal phase seeds (sunflower & sesame)", () => { + const nutrition: NutritionGuidance = { + ...baseNutrition, + seeds: "Sunflower & Sesame", + }; + + render(); + + expect(screen.getByText(/🌱 Sunflower & Sesame/)).toBeInTheDocument(); + }); + }); + + describe("carb range variations", () => { + it("displays low carb range", () => { + const nutrition: NutritionGuidance = { + ...baseNutrition, + carbRange: "50-75g", + }; + + render(); + + expect(screen.getByText(/🍽️ Carbs: 50-75g/)).toBeInTheDocument(); + }); + + it("displays high carb range", () => { + const nutrition: NutritionGuidance = { + ...baseNutrition, + carbRange: "150-200g", + }; + + render(); + + expect(screen.getByText(/🍽️ Carbs: 150-200g/)).toBeInTheDocument(); + }); + }); + + describe("keto guidance variations", () => { + it("displays keto-friendly guidance", () => { + const nutrition: NutritionGuidance = { + ...baseNutrition, + ketoGuidance: "Good day for keto", + }; + + render(); + + expect( + screen.getByText(/🥑 Keto: Good day for keto/), + ).toBeInTheDocument(); + }); + + it("displays carb-loading guidance", () => { + const nutrition: NutritionGuidance = { + ...baseNutrition, + ketoGuidance: "Consider carb loading", + }; + + render(); + + expect( + screen.getByText(/🥑 Keto: Consider carb loading/), + ).toBeInTheDocument(); + }); + }); + + describe("styling", () => { + it("renders within a bordered container", () => { + const { container } = render( + , + ); + + const panel = container.firstChild as HTMLElement; + expect(panel).toHaveClass("rounded-lg", "border", "p-4"); + }); + + it("renders heading with semibold font", () => { + render(); + + const heading = screen.getByText("NUTRITION TODAY"); + expect(heading).toHaveClass("font-semibold"); + }); + }); +}); diff --git a/src/components/dashboard/override-toggles.test.tsx b/src/components/dashboard/override-toggles.test.tsx new file mode 100644 index 0000000..a008431 --- /dev/null +++ b/src/components/dashboard/override-toggles.test.tsx @@ -0,0 +1,216 @@ +// ABOUTME: Unit tests for OverrideToggles component. +// ABOUTME: Tests toggle states, callbacks, and all override types. +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { OverrideType } from "@/types"; +import { OverrideToggles } from "./override-toggles"; + +describe("OverrideToggles", () => { + const baseProps = { + activeOverrides: [] as OverrideType[], + onToggle: vi.fn(), + }; + + describe("rendering", () => { + it("renders the OVERRIDES heading", () => { + render(); + + expect(screen.getByText("OVERRIDES")).toBeInTheDocument(); + }); + + it("renders all four override options", () => { + render(); + + expect(screen.getByText("Flare Mode")).toBeInTheDocument(); + expect(screen.getByText("High Stress")).toBeInTheDocument(); + expect(screen.getByText("Poor Sleep")).toBeInTheDocument(); + expect(screen.getByText("PMS Symptoms")).toBeInTheDocument(); + }); + + it("renders four checkboxes", () => { + render(); + + const checkboxes = screen.getAllByRole("checkbox"); + expect(checkboxes).toHaveLength(4); + }); + }); + + describe("checkbox states", () => { + it("all checkboxes are unchecked when activeOverrides is empty", () => { + render(); + + const checkboxes = screen.getAllByRole("checkbox"); + for (const checkbox of checkboxes) { + expect(checkbox).not.toBeChecked(); + } + }); + + it("flare checkbox is checked when flare is active", () => { + render(); + + const flareCheckbox = screen.getByRole("checkbox", { + name: "Flare Mode", + }); + expect(flareCheckbox).toBeChecked(); + }); + + it("stress checkbox is checked when stress is active", () => { + render(); + + const stressCheckbox = screen.getByRole("checkbox", { + name: "High Stress", + }); + expect(stressCheckbox).toBeChecked(); + }); + + it("sleep checkbox is checked when sleep is active", () => { + render(); + + const sleepCheckbox = screen.getByRole("checkbox", { + name: "Poor Sleep", + }); + expect(sleepCheckbox).toBeChecked(); + }); + + it("pms checkbox is checked when pms is active", () => { + render(); + + const pmsCheckbox = screen.getByRole("checkbox", { + name: "PMS Symptoms", + }); + expect(pmsCheckbox).toBeChecked(); + }); + + it("multiple checkboxes are checked when multiple overrides are active", () => { + render( + , + ); + + expect( + screen.getByRole("checkbox", { name: "Flare Mode" }), + ).toBeChecked(); + expect( + screen.getByRole("checkbox", { name: "High Stress" }), + ).toBeChecked(); + expect( + screen.getByRole("checkbox", { name: "Poor Sleep" }), + ).not.toBeChecked(); + expect( + screen.getByRole("checkbox", { name: "PMS Symptoms" }), + ).toBeChecked(); + }); + + it("all checkboxes are checked when all overrides are active", () => { + render( + , + ); + + const checkboxes = screen.getAllByRole("checkbox"); + for (const checkbox of checkboxes) { + expect(checkbox).toBeChecked(); + } + }); + }); + + describe("toggle interactions", () => { + it("calls onToggle with flare when flare checkbox is clicked", () => { + const onToggle = vi.fn(); + render(); + + const flareCheckbox = screen.getByRole("checkbox", { + name: "Flare Mode", + }); + fireEvent.click(flareCheckbox); + + expect(onToggle).toHaveBeenCalledWith("flare"); + expect(onToggle).toHaveBeenCalledTimes(1); + }); + + it("calls onToggle with stress when stress checkbox is clicked", () => { + const onToggle = vi.fn(); + render(); + + const stressCheckbox = screen.getByRole("checkbox", { + name: "High Stress", + }); + fireEvent.click(stressCheckbox); + + expect(onToggle).toHaveBeenCalledWith("stress"); + expect(onToggle).toHaveBeenCalledTimes(1); + }); + + it("calls onToggle with sleep when sleep checkbox is clicked", () => { + const onToggle = vi.fn(); + render(); + + const sleepCheckbox = screen.getByRole("checkbox", { + name: "Poor Sleep", + }); + fireEvent.click(sleepCheckbox); + + expect(onToggle).toHaveBeenCalledWith("sleep"); + expect(onToggle).toHaveBeenCalledTimes(1); + }); + + it("calls onToggle with pms when pms checkbox is clicked", () => { + const onToggle = vi.fn(); + render(); + + const pmsCheckbox = screen.getByRole("checkbox", { + name: "PMS Symptoms", + }); + fireEvent.click(pmsCheckbox); + + expect(onToggle).toHaveBeenCalledWith("pms"); + expect(onToggle).toHaveBeenCalledTimes(1); + }); + + it("calls onToggle when unchecking an active override", () => { + const onToggle = vi.fn(); + render( + , + ); + + const flareCheckbox = screen.getByRole("checkbox", { + name: "Flare Mode", + }); + fireEvent.click(flareCheckbox); + + expect(onToggle).toHaveBeenCalledWith("flare"); + }); + }); + + describe("styling", () => { + it("renders within a bordered container", () => { + const { container } = render(); + + const panel = container.firstChild as HTMLElement; + expect(panel).toHaveClass("rounded-lg", "border", "p-4"); + }); + + it("renders heading with semibold font", () => { + render(); + + const heading = screen.getByText("OVERRIDES"); + expect(heading).toHaveClass("font-semibold"); + }); + + it("renders labels as clickable cursor-pointer", () => { + render(); + + const labels = screen.getAllByText( + /Flare Mode|High Stress|Poor Sleep|PMS Symptoms/, + ); + for (const label of labels) { + const labelElement = label.closest("label"); + expect(labelElement).toHaveClass("cursor-pointer"); + } + }); + }); +});