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 <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 State Summary
|
## Current State Summary
|
||||||
|
|
||||||
### Overall Status: 625 tests passing across 35 test files
|
### Overall Status: 707 tests passing across 40 test files
|
||||||
|
|
||||||
### Library Implementation
|
### Library Implementation
|
||||||
| File | Status | Gap Analysis |
|
| 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/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/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/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/decision-card.test.tsx` | **EXISTS** - 11 tests (rendering, status icons, styling) |
|
||||||
| `src/components/dashboard/data-panel.test.tsx` | **MISSING** - Needs tests for biometrics display |
|
| `src/components/dashboard/data-panel.test.tsx` | **EXISTS** - 18 tests (biometrics display, null handling, styling) |
|
||||||
| `src/components/dashboard/nutrition-panel.test.tsx` | **MISSING** - Needs tests for nutrition guidance display |
|
| `src/components/dashboard/nutrition-panel.test.tsx` | **EXISTS** - 12 tests (seeds, carbs, keto guidance) |
|
||||||
| `src/components/dashboard/override-toggles.test.tsx` | **MISSING** - Needs tests for toggle state and callbacks |
|
| `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/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) |
|
| `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 |
|
| E2E tests | **AUTHORIZED SKIP** - Per specs/testing.md |
|
||||||
|
|
||||||
@@ -665,20 +665,21 @@ Testing, error handling, and refinements.
|
|||||||
- **Why:** Confidence in production deployment
|
- **Why:** Confidence in production deployment
|
||||||
- **Status:** Per specs/testing.md: "End-to-end tests are not required for MVP (authorized skip)"
|
- **Status:** Per specs/testing.md: "End-to-end tests are not required for MVP (authorized skip)"
|
||||||
|
|
||||||
### P3.11: Missing Component Tests
|
### P3.11: Missing Component Tests ✅ COMPLETE
|
||||||
- [ ] Add unit tests for untested components
|
- [x] Add unit tests for untested components
|
||||||
- **Components Needing Tests (5 total):**
|
- **Components Tested (5 total):**
|
||||||
- `src/components/dashboard/decision-card.tsx` - Tests for rendering decision status, icon, and reason
|
- `src/components/dashboard/decision-card.tsx` - 11 tests for rendering decision status, icon, reason, styling
|
||||||
- `src/components/dashboard/data-panel.tsx` - Tests for biometrics display (BB, HRV, intensity)
|
- `src/components/dashboard/data-panel.tsx` - 18 tests for biometrics display (BB, HRV, intensity), null handling, styling
|
||||||
- `src/components/dashboard/nutrition-panel.tsx` - Tests for seeds, carbs, keto guidance display
|
- `src/components/dashboard/nutrition-panel.tsx` - 12 tests for seeds, carbs, keto guidance display
|
||||||
- `src/components/dashboard/override-toggles.tsx` - Tests for toggle states and callbacks (has interactive state)
|
- `src/components/dashboard/override-toggles.tsx` - 18 tests for toggle states, callbacks, styling
|
||||||
- `src/components/calendar/day-cell.tsx` - Tests for phase coloring and click handler
|
- `src/components/calendar/day-cell.tsx` - 23 tests for phase coloring, today highlighting, click handling
|
||||||
- **Test Files to Create:**
|
- **Test Files Created:**
|
||||||
- `src/components/dashboard/decision-card.test.tsx`
|
- `src/components/dashboard/decision-card.test.tsx` - 11 tests
|
||||||
- `src/components/dashboard/data-panel.test.tsx`
|
- `src/components/dashboard/data-panel.test.tsx` - 18 tests
|
||||||
- `src/components/dashboard/nutrition-panel.test.tsx`
|
- `src/components/dashboard/nutrition-panel.test.tsx` - 12 tests
|
||||||
- `src/components/dashboard/override-toggles.test.tsx`
|
- `src/components/dashboard/override-toggles.test.tsx` - 18 tests
|
||||||
- `src/components/calendar/day-cell.test.tsx`
|
- `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
|
- **Why:** Component isolation ensures UI correctness and prevents regressions
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -797,7 +798,6 @@ P4.* UX Polish ────────> After core functionality complete
|
|||||||
| Priority | Task | Effort | Notes |
|
| Priority | Task | Effort | Notes |
|
||||||
|----------|------|--------|-------|
|
|----------|------|--------|-------|
|
||||||
| Medium | P2.18 OIDC Auth | Large | Production auth requirement |
|
| 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.7 Error Handling | Small | Polish |
|
||||||
| Low | P3.8 Loading States | Small | Polish |
|
| Low | P3.8 Loading States | Small | Polish |
|
||||||
| Low | P4.* UX Polish | Various | After core complete |
|
| 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 |
|
| P0.4 | P0.1, P0.2 | P1.7, P2.9, P2.10, P2.13 |
|
||||||
| P2.18 | P1.6 | - |
|
| P2.18 | P1.6 | - |
|
||||||
| P3.9 | P2.4 | - |
|
| 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.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.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.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
|
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
|
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)
|
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)
|
||||||
|
|||||||
190
src/components/calendar/day-cell.test.tsx
Normal file
190
src/components/calendar/day-cell.test.tsx
Normal file
@@ -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(<DayCell {...baseProps} date={new Date("2026-01-15")} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("15")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the cycle day label", () => {
|
||||||
|
render(<DayCell {...baseProps} cycleDay={5} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Day 5")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders as a button", () => {
|
||||||
|
render(<DayCell {...baseProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders cycle day 1 correctly", () => {
|
||||||
|
render(<DayCell {...baseProps} cycleDay={1} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Day 1")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders high cycle day numbers", () => {
|
||||||
|
render(<DayCell {...baseProps} cycleDay={28} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Day 28")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("phase colors", () => {
|
||||||
|
it("applies blue background for MENSTRUAL phase", () => {
|
||||||
|
render(<DayCell {...baseProps} phase="MENSTRUAL" />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).toHaveClass("bg-blue-100");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies green background for FOLLICULAR phase", () => {
|
||||||
|
render(<DayCell {...baseProps} phase="FOLLICULAR" />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).toHaveClass("bg-green-100");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies purple background for OVULATION phase", () => {
|
||||||
|
render(<DayCell {...baseProps} phase="OVULATION" />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).toHaveClass("bg-purple-100");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies yellow background for EARLY_LUTEAL phase", () => {
|
||||||
|
render(<DayCell {...baseProps} phase="EARLY_LUTEAL" />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).toHaveClass("bg-yellow-100");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies red background for LATE_LUTEAL phase", () => {
|
||||||
|
render(<DayCell {...baseProps} phase="LATE_LUTEAL" />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).toHaveClass("bg-red-100");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("today highlighting", () => {
|
||||||
|
it("does not have ring when isToday is false", () => {
|
||||||
|
render(<DayCell {...baseProps} isToday={false} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).not.toHaveClass("ring-2", "ring-black");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has ring-2 ring-black when isToday is true", () => {
|
||||||
|
render(<DayCell {...baseProps} isToday={true} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).toHaveClass("ring-2", "ring-black");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maintains phase color when isToday is true", () => {
|
||||||
|
render(<DayCell {...baseProps} phase="OVULATION" isToday={true} />);
|
||||||
|
|
||||||
|
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(<DayCell {...baseProps} onClick={onClick} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
fireEvent.click(button);
|
||||||
|
|
||||||
|
expect(onClick).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not throw when onClick is undefined", () => {
|
||||||
|
render(<DayCell {...baseProps} onClick={undefined} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(() => fireEvent.click(button)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onClick once per click", () => {
|
||||||
|
const onClick = vi.fn();
|
||||||
|
render(<DayCell {...baseProps} onClick={onClick} />);
|
||||||
|
|
||||||
|
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(<DayCell {...baseProps} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).toHaveClass("rounded");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has padding", () => {
|
||||||
|
render(<DayCell {...baseProps} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).toHaveClass("p-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders day number with font-medium", () => {
|
||||||
|
render(<DayCell {...baseProps} date={new Date("2026-01-15")} />);
|
||||||
|
|
||||||
|
const dayNumber = screen.getByText("15");
|
||||||
|
expect(dayNumber).toHaveClass("font-medium");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders cycle day label in gray", () => {
|
||||||
|
render(<DayCell {...baseProps} cycleDay={5} />);
|
||||||
|
|
||||||
|
const cycleLabel = screen.getByText("Day 5");
|
||||||
|
expect(cycleLabel).toHaveClass("text-gray-500");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("date variations", () => {
|
||||||
|
it("renders single digit day", () => {
|
||||||
|
render(<DayCell {...baseProps} date={new Date("2026-01-05")} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("5")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders last day of month", () => {
|
||||||
|
render(<DayCell {...baseProps} date={new Date("2026-01-31")} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("31")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders first day of month", () => {
|
||||||
|
render(<DayCell {...baseProps} date={new Date("2026-02-01")} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("1")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
143
src/components/dashboard/data-panel.test.tsx
Normal file
143
src/components/dashboard/data-panel.test.tsx
Normal file
@@ -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(<DataPanel {...baseProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("YOUR DATA")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders body battery current value", () => {
|
||||||
|
render(<DataPanel {...baseProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Body Battery: 75/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders yesterday low value", () => {
|
||||||
|
render(<DataPanel {...baseProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Yesterday Low: 25/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders HRV status", () => {
|
||||||
|
render(<DataPanel {...baseProps} hrvStatus="Balanced" />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/HRV: Balanced/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders week intensity with phase limit", () => {
|
||||||
|
render(<DataPanel {...baseProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Week: 120\/200 min/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders remaining minutes", () => {
|
||||||
|
render(<DataPanel {...baseProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Remaining: 80 min/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("null value handling", () => {
|
||||||
|
it("displays N/A when bodyBatteryCurrent is null", () => {
|
||||||
|
render(<DataPanel {...baseProps} bodyBatteryCurrent={null} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Body Battery: N\/A/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays N/A when bodyBatteryYesterdayLow is null", () => {
|
||||||
|
render(<DataPanel {...baseProps} bodyBatteryYesterdayLow={null} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Yesterday Low: N\/A/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays N/A for both when both are null", () => {
|
||||||
|
render(
|
||||||
|
<DataPanel
|
||||||
|
{...baseProps}
|
||||||
|
bodyBatteryCurrent={null}
|
||||||
|
bodyBatteryYesterdayLow={null}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(<DataPanel {...baseProps} hrvStatus="Balanced" />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/HRV: Balanced/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays Unbalanced HRV status", () => {
|
||||||
|
render(<DataPanel {...baseProps} hrvStatus="Unbalanced" />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/HRV: Unbalanced/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays Unknown HRV status", () => {
|
||||||
|
render(<DataPanel {...baseProps} hrvStatus="Unknown" />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/HRV: Unknown/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("intensity values", () => {
|
||||||
|
it("displays zero intensity correctly", () => {
|
||||||
|
render(<DataPanel {...baseProps} weekIntensity={0} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Week: 0\/200 min/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays when over phase limit", () => {
|
||||||
|
render(<DataPanel {...baseProps} weekIntensity={250} phaseLimit={200} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Week: 250\/200 min/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays zero remaining minutes", () => {
|
||||||
|
render(<DataPanel {...baseProps} remainingMinutes={0} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Remaining: 0 min/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays negative remaining minutes", () => {
|
||||||
|
render(<DataPanel {...baseProps} remainingMinutes={-50} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Remaining: -50 min/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("styling", () => {
|
||||||
|
it("renders within a bordered container", () => {
|
||||||
|
const { container } = render(<DataPanel {...baseProps} />);
|
||||||
|
|
||||||
|
const panel = container.firstChild as HTMLElement;
|
||||||
|
expect(panel).toHaveClass("rounded-lg", "border", "p-4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders heading with semibold font", () => {
|
||||||
|
render(<DataPanel {...baseProps} />);
|
||||||
|
|
||||||
|
const heading = screen.getByText("YOUR DATA");
|
||||||
|
expect(heading).toHaveClass("font-semibold");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
166
src/components/dashboard/decision-card.test.tsx
Normal file
166
src/components/dashboard/decision-card.test.tsx
Normal file
@@ -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(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("🛌")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the decision status", () => {
|
||||||
|
const decision: Decision = {
|
||||||
|
status: "TRAIN",
|
||||||
|
reason: "Good to go",
|
||||||
|
icon: "🏃",
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("TRAIN")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the decision reason", () => {
|
||||||
|
const decision: Decision = {
|
||||||
|
status: "GENTLE",
|
||||||
|
reason: "Take it easy today",
|
||||||
|
icon: "🧘",
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
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(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
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(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
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(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
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(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
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(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
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(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
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(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
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(<DecisionCard decision={decision} />);
|
||||||
|
|
||||||
|
const reason = screen.getByText("Muted reason text");
|
||||||
|
expect(reason).toHaveClass("text-gray-600");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
136
src/components/dashboard/nutrition-panel.test.tsx
Normal file
136
src/components/dashboard/nutrition-panel.test.tsx
Normal file
@@ -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(<NutritionPanel nutrition={baseNutrition} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("NUTRITION TODAY")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders seeds guidance with emoji", () => {
|
||||||
|
render(<NutritionPanel nutrition={baseNutrition} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/🌱 Flax & Pumpkin/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders carb range with emoji", () => {
|
||||||
|
render(<NutritionPanel nutrition={baseNutrition} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/🍽️ Carbs: 100-150g/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders keto guidance with emoji", () => {
|
||||||
|
render(<NutritionPanel nutrition={baseNutrition} />);
|
||||||
|
|
||||||
|
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(<NutritionPanel nutrition={nutrition} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/🌱 Flax & Pumpkin/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays luteal phase seeds (sunflower & sesame)", () => {
|
||||||
|
const nutrition: NutritionGuidance = {
|
||||||
|
...baseNutrition,
|
||||||
|
seeds: "Sunflower & Sesame",
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<NutritionPanel nutrition={nutrition} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/🌱 Sunflower & Sesame/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("carb range variations", () => {
|
||||||
|
it("displays low carb range", () => {
|
||||||
|
const nutrition: NutritionGuidance = {
|
||||||
|
...baseNutrition,
|
||||||
|
carbRange: "50-75g",
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<NutritionPanel nutrition={nutrition} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/🍽️ Carbs: 50-75g/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays high carb range", () => {
|
||||||
|
const nutrition: NutritionGuidance = {
|
||||||
|
...baseNutrition,
|
||||||
|
carbRange: "150-200g",
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<NutritionPanel nutrition={nutrition} />);
|
||||||
|
|
||||||
|
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(<NutritionPanel nutrition={nutrition} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(/🥑 Keto: Good day for keto/),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays carb-loading guidance", () => {
|
||||||
|
const nutrition: NutritionGuidance = {
|
||||||
|
...baseNutrition,
|
||||||
|
ketoGuidance: "Consider carb loading",
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<NutritionPanel nutrition={nutrition} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(/🥑 Keto: Consider carb loading/),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("styling", () => {
|
||||||
|
it("renders within a bordered container", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<NutritionPanel nutrition={baseNutrition} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const panel = container.firstChild as HTMLElement;
|
||||||
|
expect(panel).toHaveClass("rounded-lg", "border", "p-4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders heading with semibold font", () => {
|
||||||
|
render(<NutritionPanel nutrition={baseNutrition} />);
|
||||||
|
|
||||||
|
const heading = screen.getByText("NUTRITION TODAY");
|
||||||
|
expect(heading).toHaveClass("font-semibold");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
216
src/components/dashboard/override-toggles.test.tsx
Normal file
216
src/components/dashboard/override-toggles.test.tsx
Normal file
@@ -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(<OverrideToggles {...baseProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("OVERRIDES")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders all four override options", () => {
|
||||||
|
render(<OverrideToggles {...baseProps} />);
|
||||||
|
|
||||||
|
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(<OverrideToggles {...baseProps} />);
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole("checkbox");
|
||||||
|
expect(checkboxes).toHaveLength(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkbox states", () => {
|
||||||
|
it("all checkboxes are unchecked when activeOverrides is empty", () => {
|
||||||
|
render(<OverrideToggles {...baseProps} activeOverrides={[]} />);
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole("checkbox");
|
||||||
|
for (const checkbox of checkboxes) {
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flare checkbox is checked when flare is active", () => {
|
||||||
|
render(<OverrideToggles {...baseProps} activeOverrides={["flare"]} />);
|
||||||
|
|
||||||
|
const flareCheckbox = screen.getByRole("checkbox", {
|
||||||
|
name: "Flare Mode",
|
||||||
|
});
|
||||||
|
expect(flareCheckbox).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stress checkbox is checked when stress is active", () => {
|
||||||
|
render(<OverrideToggles {...baseProps} activeOverrides={["stress"]} />);
|
||||||
|
|
||||||
|
const stressCheckbox = screen.getByRole("checkbox", {
|
||||||
|
name: "High Stress",
|
||||||
|
});
|
||||||
|
expect(stressCheckbox).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sleep checkbox is checked when sleep is active", () => {
|
||||||
|
render(<OverrideToggles {...baseProps} activeOverrides={["sleep"]} />);
|
||||||
|
|
||||||
|
const sleepCheckbox = screen.getByRole("checkbox", {
|
||||||
|
name: "Poor Sleep",
|
||||||
|
});
|
||||||
|
expect(sleepCheckbox).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pms checkbox is checked when pms is active", () => {
|
||||||
|
render(<OverrideToggles {...baseProps} activeOverrides={["pms"]} />);
|
||||||
|
|
||||||
|
const pmsCheckbox = screen.getByRole("checkbox", {
|
||||||
|
name: "PMS Symptoms",
|
||||||
|
});
|
||||||
|
expect(pmsCheckbox).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("multiple checkboxes are checked when multiple overrides are active", () => {
|
||||||
|
render(
|
||||||
|
<OverrideToggles
|
||||||
|
{...baseProps}
|
||||||
|
activeOverrides={["flare", "stress", "pms"]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<OverrideToggles
|
||||||
|
{...baseProps}
|
||||||
|
activeOverrides={["flare", "stress", "sleep", "pms"]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(<OverrideToggles activeOverrides={[]} onToggle={onToggle} />);
|
||||||
|
|
||||||
|
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(<OverrideToggles activeOverrides={[]} onToggle={onToggle} />);
|
||||||
|
|
||||||
|
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(<OverrideToggles activeOverrides={[]} onToggle={onToggle} />);
|
||||||
|
|
||||||
|
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(<OverrideToggles activeOverrides={[]} onToggle={onToggle} />);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<OverrideToggles activeOverrides={["flare"]} onToggle={onToggle} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(<OverrideToggles {...baseProps} />);
|
||||||
|
|
||||||
|
const panel = container.firstChild as HTMLElement;
|
||||||
|
expect(panel).toHaveClass("rounded-lg", "border", "p-4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders heading with semibold font", () => {
|
||||||
|
render(<OverrideToggles {...baseProps} />);
|
||||||
|
|
||||||
|
const heading = screen.getByText("OVERRIDES");
|
||||||
|
expect(heading).toHaveClass("font-semibold");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders labels as clickable cursor-pointer", () => {
|
||||||
|
render(<OverrideToggles {...baseProps} />);
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user