// ABOUTME: Unit tests for cycle phase calculation utilities. // ABOUTME: Tests getCycleDay, getPhase, and phase limit functions with variable cycle lengths. import { describe, expect, it } from "vitest"; import { getCycleDay, getPhase, getPhaseLimit } from "./cycle"; describe("getCycleDay", () => { it("returns 1 on the first day of the cycle", () => { const lastPeriod = new Date("2025-01-01"); const currentDate = new Date("2025-01-01"); expect(getCycleDay(lastPeriod, 31, currentDate)).toBe(1); }); it("returns correct day within cycle", () => { const lastPeriod = new Date("2025-01-01"); const currentDate = new Date("2025-01-15"); expect(getCycleDay(lastPeriod, 31, currentDate)).toBe(15); }); it("wraps around after cycle length", () => { const lastPeriod = new Date("2025-01-01"); const currentDate = new Date("2025-02-01"); // 31 days later expect(getCycleDay(lastPeriod, 31, currentDate)).toBe(1); }); }); describe("getPhase", () => { // Phase boundaries per spec (cycle-tracking.md): // MENSTRUAL: 1-3 (fixed) // FOLLICULAR: 4 to (cycleLength - 16) // OVULATION: (cycleLength - 15) to (cycleLength - 14) // EARLY_LUTEAL: (cycleLength - 13) to (cycleLength - 7) // LATE_LUTEAL: (cycleLength - 6) to cycleLength describe("31-day cycle", () => { const cycleLength = 31; it("returns MENSTRUAL for days 1-3", () => { expect(getPhase(1, cycleLength)).toBe("MENSTRUAL"); expect(getPhase(3, cycleLength)).toBe("MENSTRUAL"); }); it("returns FOLLICULAR for days 4-15", () => { // 4 to (31-16) = 4-15 expect(getPhase(4, cycleLength)).toBe("FOLLICULAR"); expect(getPhase(15, cycleLength)).toBe("FOLLICULAR"); }); it("returns OVULATION for days 16-17", () => { // (31-15) to (31-14) = 16-17 expect(getPhase(16, cycleLength)).toBe("OVULATION"); expect(getPhase(17, cycleLength)).toBe("OVULATION"); }); it("returns EARLY_LUTEAL for days 18-24", () => { // (31-13) to (31-7) = 18-24 expect(getPhase(18, cycleLength)).toBe("EARLY_LUTEAL"); expect(getPhase(24, cycleLength)).toBe("EARLY_LUTEAL"); }); it("returns LATE_LUTEAL for days 25-31", () => { // (31-6) to 31 = 25-31 expect(getPhase(25, cycleLength)).toBe("LATE_LUTEAL"); expect(getPhase(31, cycleLength)).toBe("LATE_LUTEAL"); }); }); describe("28-day cycle", () => { const cycleLength = 28; it("returns MENSTRUAL for days 1-3", () => { expect(getPhase(1, cycleLength)).toBe("MENSTRUAL"); expect(getPhase(3, cycleLength)).toBe("MENSTRUAL"); }); it("returns FOLLICULAR for days 4-12", () => { // 4 to (28-16) = 4-12 expect(getPhase(4, cycleLength)).toBe("FOLLICULAR"); expect(getPhase(12, cycleLength)).toBe("FOLLICULAR"); }); it("returns OVULATION for days 13-14", () => { // (28-15) to (28-14) = 13-14 expect(getPhase(13, cycleLength)).toBe("OVULATION"); expect(getPhase(14, cycleLength)).toBe("OVULATION"); }); it("returns EARLY_LUTEAL for days 15-21", () => { // (28-13) to (28-7) = 15-21 expect(getPhase(15, cycleLength)).toBe("EARLY_LUTEAL"); expect(getPhase(21, cycleLength)).toBe("EARLY_LUTEAL"); }); it("returns LATE_LUTEAL for days 22-28", () => { // (28-6) to 28 = 22-28 expect(getPhase(22, cycleLength)).toBe("LATE_LUTEAL"); expect(getPhase(28, cycleLength)).toBe("LATE_LUTEAL"); }); }); describe("35-day cycle", () => { const cycleLength = 35; it("returns MENSTRUAL for days 1-3", () => { expect(getPhase(1, cycleLength)).toBe("MENSTRUAL"); expect(getPhase(3, cycleLength)).toBe("MENSTRUAL"); }); it("returns FOLLICULAR for days 4-19", () => { // 4 to (35-16) = 4-19 expect(getPhase(4, cycleLength)).toBe("FOLLICULAR"); expect(getPhase(19, cycleLength)).toBe("FOLLICULAR"); }); it("returns OVULATION for days 20-21", () => { // (35-15) to (35-14) = 20-21 expect(getPhase(20, cycleLength)).toBe("OVULATION"); expect(getPhase(21, cycleLength)).toBe("OVULATION"); }); it("returns EARLY_LUTEAL for days 22-28", () => { // (35-13) to (35-7) = 22-28 expect(getPhase(22, cycleLength)).toBe("EARLY_LUTEAL"); expect(getPhase(28, cycleLength)).toBe("EARLY_LUTEAL"); }); it("returns LATE_LUTEAL for days 29-35", () => { // (35-6) to 35 = 29-35 expect(getPhase(29, cycleLength)).toBe("LATE_LUTEAL"); expect(getPhase(35, cycleLength)).toBe("LATE_LUTEAL"); }); }); describe("edge cases", () => { it("defaults to LATE_LUTEAL for days beyond cycle length", () => { expect(getPhase(32, 31)).toBe("LATE_LUTEAL"); expect(getPhase(40, 35)).toBe("LATE_LUTEAL"); }); it("handles minimum cycle length (21 days)", () => { // 21-day: FOLLICULAR 4-5, OVULATION 6-7, EARLY_LUTEAL 8-14, LATE_LUTEAL 15-21 expect(getPhase(5, 21)).toBe("FOLLICULAR"); // 4 to (21-16)=5 expect(getPhase(6, 21)).toBe("OVULATION"); // (21-15)=6 to (21-14)=7 expect(getPhase(8, 21)).toBe("EARLY_LUTEAL"); // (21-13)=8 to (21-7)=14 expect(getPhase(15, 21)).toBe("LATE_LUTEAL"); // (21-6)=15 to 21 }); it("handles maximum cycle length (45 days)", () => { // 45-day: FOLLICULAR 4-29, OVULATION 30-31, EARLY_LUTEAL 32-38, LATE_LUTEAL 39-45 expect(getPhase(29, 45)).toBe("FOLLICULAR"); // 4 to (45-16)=29 expect(getPhase(30, 45)).toBe("OVULATION"); // (45-15)=30 to (45-14)=31 expect(getPhase(32, 45)).toBe("EARLY_LUTEAL"); // (45-13)=32 to (45-7)=38 expect(getPhase(39, 45)).toBe("LATE_LUTEAL"); // (45-6)=39 to 45 }); }); }); describe("getPhaseLimit", () => { it("returns correct weekly limits for each phase", () => { // Default intensity goals (can be overridden per user) expect(getPhaseLimit("MENSTRUAL")).toBe(75); expect(getPhaseLimit("FOLLICULAR")).toBe(150); expect(getPhaseLimit("OVULATION")).toBe(100); expect(getPhaseLimit("EARLY_LUTEAL")).toBe(120); expect(getPhaseLimit("LATE_LUTEAL")).toBe(50); }); });