124 lines
5.1 KiB
JavaScript
124 lines
5.1 KiB
JavaScript
const assert = require("node:assert/strict");
|
|
const cycle = require("../static/cycle.js");
|
|
|
|
const { toDay, toIso } = cycle;
|
|
|
|
// Logs `length` period days starting at each date.
|
|
function logPeriods(starts, length = 5, flow = "medium") {
|
|
const days = {};
|
|
for (const start of starts) {
|
|
for (let offset = 0; offset < length; offset += 1) days[toIso(toDay(start) + offset)] = { flow, symptoms: [] };
|
|
}
|
|
return days;
|
|
}
|
|
|
|
// Date helpers round-trip across month, year and leap-day boundaries.
|
|
for (const iso of ["2024-02-29", "2025-12-31", "2026-03-29", "2026-10-25"]) assert.equal(toIso(toDay(iso)), iso);
|
|
assert.equal(toDay("2026-03-01") - toDay("2026-02-28"), 1);
|
|
|
|
// Period days join into periods; one unlogged day does not split a period,
|
|
// spotting never counts, and two missing days do split it.
|
|
{
|
|
const days = logPeriods(["2026-01-01"], 3);
|
|
days["2026-01-05"] = { flow: "light", symptoms: [] };
|
|
days["2026-01-10"] = { flow: "spotting", symptoms: [] };
|
|
days["2026-01-13"] = { flow: "period", symptoms: [] };
|
|
const periods = cycle.periods(days);
|
|
assert.deepEqual(periods.map(period => [toIso(period.start), period.length]), [["2026-01-01", 5], ["2026-01-13", 1]]);
|
|
}
|
|
|
|
// Nothing logged: no predictions at all.
|
|
{
|
|
const summary = cycle.summarize({}, "2026-10-18");
|
|
assert.equal(summary.current, null);
|
|
assert.equal(summary.confidence, "none");
|
|
assert.deepEqual(summary.predictions, []);
|
|
}
|
|
|
|
// One period: falls back to 28 days with low confidence and a wide range.
|
|
{
|
|
const summary = cycle.summarize(logPeriods(["2026-10-01"]), "2026-10-10");
|
|
assert.equal(summary.confidence, "none");
|
|
assert.equal(summary.cycleLength, 28);
|
|
assert.equal(toIso(summary.predictions[0].start), "2026-10-29");
|
|
assert.equal(summary.predictions[0].latest - summary.predictions[0].start, 4);
|
|
assert.equal(summary.current.cycleDay, 10);
|
|
}
|
|
|
|
// Regular 30-day cycles predict 30 days ahead, with the minimum ±2 range
|
|
// that widens for later cycles, and ovulation 14 days before each period.
|
|
{
|
|
const starts = ["2026-04-01", "2026-05-01", "2026-05-31", "2026-06-30", "2026-07-30", "2026-08-29", "2026-09-28"];
|
|
const summary = cycle.summarize(logPeriods(starts), "2026-10-10");
|
|
assert.equal(summary.confidence, "normal");
|
|
assert.equal(summary.sampleSize, 6);
|
|
assert.equal(summary.cycleLength, 30);
|
|
assert.equal(summary.periodLength, 5);
|
|
const [next, second] = summary.predictions;
|
|
assert.equal(toIso(next.start), "2026-10-28");
|
|
assert.equal(toIso(next.end), "2026-11-01");
|
|
assert.equal(next.start - next.earliest, 2);
|
|
assert.equal(toIso(second.start), "2026-11-27");
|
|
assert.equal(second.start - second.earliest, 3);
|
|
assert.equal(summary.predictions.length, 6);
|
|
const window = summary.fertile[0];
|
|
assert.equal(toIso(window.ovulation), "2026-10-14");
|
|
assert.equal(toIso(window.start), "2026-10-09");
|
|
assert.equal(toIso(window.end), "2026-10-15");
|
|
}
|
|
|
|
// Recent cycles weigh more: a shift from 26 to 32 days pulls the mean
|
|
// above the plain average of 29.
|
|
{
|
|
const starts = ["2026-01-01", "2026-01-27", "2026-02-22", "2026-03-20", "2026-04-21", "2026-05-23", "2026-06-24"];
|
|
const summary = cycle.summarize(logPeriods(starts), "2026-07-01");
|
|
assert.ok(summary.cycleLength >= 30, `expected recent cycles to dominate, got ${summary.cycleLength}`);
|
|
}
|
|
|
|
// A gap from a forgotten period (two cycles' worth) is excluded from the average.
|
|
{
|
|
const starts = ["2026-01-01", "2026-01-29", "2026-04-23", "2026-05-21", "2026-06-18"];
|
|
const summary = cycle.summarize(logPeriods(starts), "2026-06-20");
|
|
assert.deepEqual(summary.cycles.map(item => item.used), [true, false, true, true]);
|
|
assert.equal(summary.cycleLength, 28);
|
|
}
|
|
|
|
// Late: once the expected day has passed, the next period is expected from
|
|
// tomorrow, while this cycle's fertile window stays where it was.
|
|
{
|
|
const starts = ["2026-07-01", "2026-07-29", "2026-08-26", "2026-09-23"];
|
|
const summary = cycle.summarize(logPeriods(starts), "2026-10-24");
|
|
assert.equal(summary.late, 3);
|
|
assert.equal(toIso(summary.predictions[0].start), "2026-10-25");
|
|
assert.equal(toIso(summary.fertile[0].ovulation), "2026-10-07");
|
|
}
|
|
|
|
// Due today is not late yet.
|
|
{
|
|
const summary = cycle.summarize(logPeriods(["2026-08-26", "2026-09-23"]), "2026-10-21");
|
|
assert.equal(summary.late, 0);
|
|
assert.equal(toIso(summary.predictions[0].start), "2026-10-21");
|
|
}
|
|
|
|
// An ongoing period is flagged and does not count towards period length yet.
|
|
{
|
|
const days = logPeriods(["2026-08-01", "2026-08-29"], 6);
|
|
Object.assign(days, logPeriods(["2026-09-26"], 2));
|
|
const summary = cycle.summarize(days, "2026-09-27");
|
|
assert.equal(summary.current.onPeriod, true);
|
|
assert.equal(summary.periodLength, 6);
|
|
}
|
|
|
|
// Days logged after "today" (a phone ahead of the server) are ignored.
|
|
{
|
|
const summary = cycle.summarize(logPeriods(["2026-09-01", "2026-10-19"]), "2026-10-18");
|
|
assert.equal(toIso(summary.current.start), "2026-09-01");
|
|
}
|
|
|
|
// Very short cycles never place the fertile window inside the period.
|
|
{
|
|
const summary = cycle.summarize(logPeriods(["2026-08-01", "2026-08-19", "2026-09-06", "2026-09-24"], 5), "2026-09-30");
|
|
for (const window of summary.fertile) assert.ok(window.start > toDay("2026-09-28"));
|
|
}
|
|
|
|
console.log("cycle tests passed");
|