Co-authored-by: dev_agent <17+dev_agent@code.cubecraftcreations.com> Co-committed-by: dev_agent <17+dev_agent@code.cubecraftcreations.com>
116 lines
4.8 KiB
TypeScript
116 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
cycleWeekIndex,
|
|
dayState,
|
|
monthDates,
|
|
monthStart,
|
|
presenceRule,
|
|
resizePattern,
|
|
togglePatternDay,
|
|
weekdayOf,
|
|
} from "./cycle";
|
|
import type { PresenceOverride, PresenceScheduleBody } from "../api/presence";
|
|
|
|
// Anchor Monday 2026-09-07; week 0 home every day, week 1 home Sat/Sun only.
|
|
const SCHEDULE: PresenceScheduleBody = {
|
|
cycle_length_weeks: 2,
|
|
anchor_date: "2026-09-07",
|
|
pattern: { 0: [0, 1, 2, 3, 4, 5, 6], 1: [5, 6] },
|
|
};
|
|
|
|
describe("cycleWeekIndex", () => {
|
|
it("follows the anchor across a month, wrapping every 2 weeks", () => {
|
|
const expected: Record<string, number> = {};
|
|
for (const date of monthDates("2026-09-01")) {
|
|
const day = Number(date.slice(8));
|
|
// Sep 7-13 week 0, 14-20 week 1, 21-27 week 0 (wrap), 28-30 week 1;
|
|
// Sep 1-6 lie before the anchor and walk back into week 1.
|
|
expected[date] = day < 7 ? 1 : Math.floor((day - 7) / 7) % 2;
|
|
}
|
|
for (const [date, week] of Object.entries(expected)) {
|
|
expect(cycleWeekIndex(SCHEDULE.anchor_date, date, 2), date).toBe(week);
|
|
}
|
|
expect(cycleWeekIndex("2026-09-07", "2026-09-21", 2)).toBe(0);
|
|
expect(cycleWeekIndex("2026-09-07", "2026-09-28", 2)).toBe(1);
|
|
});
|
|
|
|
it("walks the cycle backwards before the anchor (floor division)", () => {
|
|
expect(cycleWeekIndex("2026-09-07", "2026-09-06", 2)).toBe(1); // day -1
|
|
expect(cycleWeekIndex("2026-09-07", "2026-08-31", 2)).toBe(1); // day -7
|
|
expect(cycleWeekIndex("2026-09-07", "2026-08-30", 2)).toBe(0); // day -8
|
|
expect(cycleWeekIndex("2026-09-07", "2026-08-24", 2)).toBe(0); // day -14
|
|
expect(cycleWeekIndex("2026-09-07", "2026-08-23", 2)).toBe(1); // day -15
|
|
expect(cycleWeekIndex("2026-09-07", "2026-09-06", 3)).toBe(2); // last week of a 3-week cycle
|
|
});
|
|
|
|
it("ignores ISO week parity across a 53-week year", () => {
|
|
// 2026-12-28 is ISO week 53 and 2027-01-04 ISO week 1 — both odd, so
|
|
// parity would call them the same week. From the anchor they alternate.
|
|
expect(cycleWeekIndex("2026-09-07", "2026-12-28", 2)).toBe(0);
|
|
expect(cycleWeekIndex("2026-09-07", "2027-01-04", 2)).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("calendar helpers", () => {
|
|
it("weekdayOf is Monday=0", () => {
|
|
expect(weekdayOf("2026-09-07")).toBe(0);
|
|
expect(weekdayOf("2026-09-01")).toBe(1);
|
|
expect(weekdayOf("2026-09-06")).toBe(6);
|
|
});
|
|
|
|
it("monthStart shifts across year ends", () => {
|
|
expect(monthStart("2026-12-15", 1)).toBe("2027-01-01");
|
|
expect(monthStart("2026-01-31", -1)).toBe("2025-12-01");
|
|
expect(monthDates("2026-02-01")).toHaveLength(28);
|
|
});
|
|
});
|
|
|
|
describe("dayState", () => {
|
|
const overrides: PresenceOverride[] = [
|
|
{ id: 1, child_id: 1, start_date: "2026-09-10", end_date: "2026-09-12", is_present: false, note: null },
|
|
{ id: 2, child_id: 1, start_date: "2026-09-16", end_date: "2026-09-16", is_present: true, note: null },
|
|
];
|
|
|
|
it("applies overrides first, then the anchor-based pattern", () => {
|
|
expect(dayState(SCHEDULE, overrides, "2026-09-08")).toBe("home");
|
|
expect(dayState(SCHEDULE, overrides, "2026-09-11")).toBe("override-away");
|
|
expect(dayState(SCHEDULE, overrides, "2026-09-15")).toBe("away");
|
|
expect(dayState(SCHEDULE, overrides, "2026-09-16")).toBe("override-home");
|
|
expect(dayState(SCHEDULE, overrides, "2026-09-19")).toBe("home");
|
|
expect(dayState(SCHEDULE, overrides, "2026-09-01")).toBe("away");
|
|
expect(dayState(SCHEDULE, overrides, "2026-09-05")).toBe("home");
|
|
});
|
|
|
|
it("treats a child with no schedule as home every day", () => {
|
|
expect(dayState(null, [], "2026-09-15")).toBe("home");
|
|
});
|
|
});
|
|
|
|
describe("pattern editing and rule text", () => {
|
|
it("toggles one weekday of one cycle week", () => {
|
|
expect(togglePatternDay(SCHEDULE.pattern, 1, 1)).toEqual({
|
|
0: [0, 1, 2, 3, 4, 5, 6],
|
|
1: [1, 5, 6],
|
|
});
|
|
expect(togglePatternDay(SCHEDULE.pattern, 1, 5)[1]).toEqual([6]);
|
|
});
|
|
|
|
it("resizes the pattern to the cycle length", () => {
|
|
expect(resizePattern(SCHEDULE.pattern, 1)).toEqual({ 0: [0, 1, 2, 3, 4, 5, 6] });
|
|
expect(resizePattern(SCHEDULE.pattern, 3)[2]).toEqual([0, 1, 2, 3, 4, 5, 6]);
|
|
});
|
|
|
|
it("describes the presence rule in plain language", () => {
|
|
expect(presenceRule(null)).toBe("Every day");
|
|
expect(presenceRule({ ...SCHEDULE, pattern: { 0: [0, 1, 2, 3, 4, 5, 6], 1: [0, 1, 2, 3, 4, 5, 6] } })).toBe(
|
|
"Every day",
|
|
);
|
|
expect(presenceRule(SCHEDULE)).toBe("Weeks 1 & 2 of cycle");
|
|
expect(
|
|
presenceRule({ cycle_length_weeks: 4, anchor_date: "2026-09-07", pattern: { 0: [0], 1: [], 2: [3], 3: [] } }),
|
|
).toBe("Weeks 1 & 3 of cycle");
|
|
expect(presenceRule({ ...SCHEDULE, pattern: { 0: [1], 1: [] } })).toBe("Week 1 of cycle");
|
|
expect(presenceRule({ ...SCHEDULE, pattern: { 0: [], 1: [] } })).toBe("Never home");
|
|
});
|
|
});
|