Fix critical bug: cycle phase boundaries now scale with cycle length

CRITICAL BUG FIX:
- Phase boundaries were hardcoded for 31-day cycle, breaking correct
  phase calculations for users with different cycle lengths (28, 35, etc.)
- Added getPhaseBoundaries(cycleLength) function in cycle.ts
- Updated getPhase() to accept cycleLength parameter (default 31)
- Updated all callers (API routes, components) to pass cycleLength
- Added 13 new tests for phase boundaries with 28, 31, and 35-day cycles

ICS IMPROVEMENTS:
- Fixed emojis to match calendar.md spec: 🩸🌱🌸🌙🌑
- Added CATEGORIES field for calendar app colors per spec:
  MENSTRUAL=Red, FOLLICULAR=Green, OVULATION=Pink,
  EARLY_LUTEAL=Yellow, LATE_LUTEAL=Orange
- Added 5 new tests for CATEGORIES

Updated IMPLEMENTATION_PLAN.md with discovered issues and test counts.

825 tests passing (up from 807)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 22:39:09 +00:00
parent 58f6c5605a
commit a977934c23
15 changed files with 337 additions and 148 deletions

View File

@@ -71,8 +71,9 @@ describe("MonthView", () => {
render(<MonthView {...baseProps} />);
// Jan 15 is "today" - aria-label includes date, cycle day, and phase
// For 28-day cycle, day 15 is EARLY_LUTEAL (days 15-21)
const todayCell = screen.getByRole("button", {
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
name: /January 15, 2026 - Cycle day 15 - Early Luteal phase \(today\)/i,
});
expect(todayCell).toHaveClass("ring-2", "ring-black");
});
@@ -99,40 +100,40 @@ describe("MonthView", () => {
expect(day1).toHaveClass("bg-blue-100");
});
it("applies follicular phase color to days 4-14", () => {
it("applies follicular phase color to days 4-12", () => {
render(<MonthView {...baseProps} />);
// Day 5 is FOLLICULAR (bg-green-100)
// For 28-day cycle, FOLLICULAR is days 4-12
const day5 = screen.getByRole("button", {
name: /January 5, 2026 - Cycle day 5 - Follicular phase/i,
});
expect(day5).toHaveClass("bg-green-100");
});
it("applies ovulation phase color to days 15-16", () => {
it("applies ovulation phase color to days 13-14", () => {
render(<MonthView {...baseProps} />);
// Day 15 is OVULATION (bg-purple-100)
const day15 = screen.getByRole("button", {
name: /January 15, 2026 - Cycle day 15 - Ovulation phase/i,
// For 28-day cycle, OVULATION is days 13-14
const day13 = screen.getByRole("button", {
name: /January 13, 2026 - Cycle day 13 - Ovulation phase/i,
});
expect(day15).toHaveClass("bg-purple-100");
expect(day13).toHaveClass("bg-purple-100");
});
it("applies early luteal phase color to days 17-24", () => {
it("applies early luteal phase color to days 15-21", () => {
render(<MonthView {...baseProps} />);
// Day 20 is EARLY_LUTEAL (bg-yellow-100)
const day20 = screen.getByRole("button", {
name: /January 20, 2026 - Cycle day 20 - Early Luteal phase/i,
// For 28-day cycle, EARLY_LUTEAL is days 15-21
const day18 = screen.getByRole("button", {
name: /January 18, 2026 - Cycle day 18 - Early Luteal phase/i,
});
expect(day20).toHaveClass("bg-yellow-100");
expect(day18).toHaveClass("bg-yellow-100");
});
it("applies late luteal phase color to days 25-31", () => {
it("applies late luteal phase color to days 22-28", () => {
render(<MonthView {...baseProps} />);
// Day 25 is LATE_LUTEAL (bg-red-100)
// For 28-day cycle, LATE_LUTEAL is days 22-28
const day25 = screen.getByRole("button", {
name: /January 25, 2026 - Cycle day 25 - Late Luteal phase/i,
});
@@ -267,20 +268,22 @@ describe("MonthView", () => {
});
describe("keyboard navigation", () => {
// For 28-day cycle:
// MENSTRUAL: 1-3, FOLLICULAR: 4-12, OVULATION: 13-14, EARLY_LUTEAL: 15-21, LATE_LUTEAL: 22-28
it("moves focus to next day when pressing ArrowRight", () => {
render(<MonthView {...baseProps} />);
// Focus on Jan 15 (today)
// Focus on Jan 15 (today) - for 28-day cycle, day 15 is EARLY_LUTEAL
const jan15 = screen.getByRole("button", {
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
name: /January 15, 2026 - Cycle day 15 - Early Luteal phase \(today\)/i,
});
jan15.focus();
// Press ArrowRight to move to Jan 16
// Press ArrowRight to move to Jan 16 - day 16 is EARLY_LUTEAL
fireEvent.keyDown(jan15, { key: "ArrowRight" });
const jan16 = screen.getByRole("button", {
name: /January 16, 2026 - Cycle day 16 - Ovulation phase$/i,
name: /January 16, 2026 - Cycle day 16 - Early Luteal phase$/i,
});
expect(document.activeElement).toBe(jan16);
});
@@ -288,17 +291,17 @@ describe("MonthView", () => {
it("moves focus to previous day when pressing ArrowLeft", () => {
render(<MonthView {...baseProps} />);
// Focus on Jan 15 (today)
// Focus on Jan 15 (today) - for 28-day cycle, day 15 is EARLY_LUTEAL
const jan15 = screen.getByRole("button", {
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
name: /January 15, 2026 - Cycle day 15 - Early Luteal phase \(today\)/i,
});
jan15.focus();
// Press ArrowLeft to move to Jan 14
// Press ArrowLeft to move to Jan 14 - day 14 is OVULATION
fireEvent.keyDown(jan15, { key: "ArrowLeft" });
const jan14 = screen.getByRole("button", {
name: /January 14, 2026 - Cycle day 14 - Follicular phase$/i,
name: /January 14, 2026 - Cycle day 14 - Ovulation phase$/i,
});
expect(document.activeElement).toBe(jan14);
});
@@ -306,17 +309,17 @@ describe("MonthView", () => {
it("moves focus to same day next week when pressing ArrowDown", () => {
render(<MonthView {...baseProps} />);
// Focus on Jan 15 (today)
// Focus on Jan 15 (today) - for 28-day cycle, day 15 is EARLY_LUTEAL
const jan15 = screen.getByRole("button", {
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
name: /January 15, 2026 - Cycle day 15 - Early Luteal phase \(today\)/i,
});
jan15.focus();
// Press ArrowDown to move to Jan 22 (7 days later)
// Press ArrowDown to move to Jan 22 (7 days later) - day 22 is LATE_LUTEAL
fireEvent.keyDown(jan15, { key: "ArrowDown" });
const jan22 = screen.getByRole("button", {
name: /January 22, 2026 - Cycle day 22 - Early Luteal phase$/i,
name: /January 22, 2026 - Cycle day 22 - Late Luteal phase$/i,
});
expect(document.activeElement).toBe(jan22);
});
@@ -324,13 +327,13 @@ describe("MonthView", () => {
it("moves focus to same day previous week when pressing ArrowUp", () => {
render(<MonthView {...baseProps} />);
// Focus on Jan 15 (today)
// Focus on Jan 15 (today) - for 28-day cycle, day 15 is EARLY_LUTEAL
const jan15 = screen.getByRole("button", {
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
name: /January 15, 2026 - Cycle day 15 - Early Luteal phase \(today\)/i,
});
jan15.focus();
// Press ArrowUp to move to Jan 8 (7 days earlier)
// Press ArrowUp to move to Jan 8 (7 days earlier) - day 8 is FOLLICULAR
fireEvent.keyDown(jan15, { key: "ArrowUp" });
const jan8 = screen.getByRole("button", {
@@ -375,9 +378,9 @@ describe("MonthView", () => {
it("wraps focus at row boundaries for Home and End keys", () => {
render(<MonthView {...baseProps} />);
// Focus on Jan 15
// Focus on Jan 15 - for 28-day cycle, day 15 is EARLY_LUTEAL
const jan15 = screen.getByRole("button", {
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
name: /January 15, 2026 - Cycle day 15 - Early Luteal phase \(today\)/i,
});
jan15.focus();
@@ -393,9 +396,9 @@ describe("MonthView", () => {
it("moves focus to last day when pressing End key", () => {
render(<MonthView {...baseProps} />);
// Focus on Jan 15
// Focus on Jan 15 - for 28-day cycle, day 15 is EARLY_LUTEAL
const jan15 = screen.getByRole("button", {
name: /January 15, 2026 - Cycle day 15 - Ovulation phase \(today\)/i,
name: /January 15, 2026 - Cycle day 15 - Early Luteal phase \(today\)/i,
});
jan15.focus();

View File

@@ -204,7 +204,7 @@ export function MonthView({
}
const cycleDay = getCycleDay(lastPeriodDate, cycleLength, date);
const phase = getPhase(cycleDay);
const phase = getPhase(cycleDay, cycleLength);
const isToday =
date.getFullYear() === today.getFullYear() &&
date.getMonth() === today.getMonth() &&