All checks were successful
Deploy / deploy (push) Successful in 2m38s
- Apply 2x multiplier for vigorous intensity minutes (matches Garmin) - Use calendar week (Mon-Sun) instead of trailing 7 days for intensity - Add HRV yesterday fallback when today's data returns empty - Add user-configurable phase intensity goals with new defaults: - Menstrual: 75, Follicular: 150, Ovulation: 100 - Early Luteal: 120, Late Luteal: 50 - Update garmin-sync and today routes to use user-specific phase limits Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
168 lines
6.0 KiB
TypeScript
168 lines
6.0 KiB
TypeScript
// 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);
|
|
});
|
|
});
|