src/lib/chart-period.test.ts
Rama main · réplica de lectura del repositorio original.
import { describe, expect, it } from "vitest";
import {
availablePeriods,
periodLabel,
periodRange,
resolveAnchor,
stepPeriod,
} from "@/lib/chart-period";
// Moved here, unchanged, from `dive-gas.test.ts` and the gas card's view tests when
// the functions themselves moved - the activity card windows its series the same
// way, and these rules are now what keeps the two cards' period controls
// behaving identically. The times below read as dives because that is what they
// were written against; the activity card feeds the same functions the start of
// each day it has diving on.
describe("periodRange", () => {
it("bounds a calendar year, half-open", () => {
expect(periodRange(Date.UTC(2026, 3, 17), "year")).toEqual({
start: Date.UTC(2026, 0, 1),
end: Date.UTC(2027, 0, 1),
});
});
it("bounds a calendar month, half-open", () => {
expect(periodRange(Date.UTC(2026, 3, 17), "month")).toEqual({
start: Date.UTC(2026, 3, 1),
end: Date.UTC(2026, 4, 1),
});
});
it("rolls a December month into the next year", () => {
expect(periodRange(Date.UTC(2026, 11, 25), "month")).toEqual({
start: Date.UTC(2026, 11, 1),
end: Date.UTC(2027, 0, 1),
});
});
it("reads the anchor in UTC, not the viewer's timezone", () => {
// Times fed to this come from `diveWallClockTime`, which encodes the dive's
// *own* local time as a UTC-reading instant. Using the local getters here
// would put a New Year's Eve dive in a different year for a viewer in
// Sydney than for one in Los Angeles.
const newYearsEve = Date.UTC(2025, 11, 31, 23, 30);
expect(periodRange(newYearsEve, "year").start).toBe(Date.UTC(2025, 0, 1));
});
});
describe("periodLabel", () => {
it("names the scope's period", () => {
expect(periodLabel(Date.UTC(2026, 3, 17), "all")).toBe("All time");
expect(periodLabel(Date.UTC(2026, 3, 17), "year")).toBe("2026");
expect(periodLabel(Date.UTC(2026, 3, 17), "month")).toBe("Apr 2026");
expect(periodLabel(Date.UTC(2026, 3, 17), "month", "long")).toBe(
"April 2026",
);
});
});
describe("stepPeriod", () => {
// Two trips a season apart, plus one the following year.
const times = [
Date.UTC(2025, 3, 10),
Date.UTC(2025, 3, 12),
Date.UTC(2025, 9, 5),
Date.UTC(2026, 2, 1),
];
it("skips empty periods rather than stepping through them", () => {
// April -> October, not April -> May -> June -> ... -> October.
expect(stepPeriod(Date.UTC(2025, 3, 10), "month", 1, times)).toBe(
Date.UTC(2025, 9, 5),
);
});
it("steps backwards to the last dive before the current period", () => {
expect(stepPeriod(Date.UTC(2025, 9, 5), "month", -1, times)).toBe(
Date.UTC(2025, 3, 12),
);
});
it("steps by year when the scope is a year", () => {
expect(stepPeriod(Date.UTC(2025, 3, 10), "year", 1, times)).toBe(
Date.UTC(2026, 2, 1),
);
});
it("returns null at either end, which is what disables the button", () => {
expect(stepPeriod(Date.UTC(2026, 2, 1), "year", 1, times)).toBeNull();
expect(stepPeriod(Date.UTC(2025, 3, 10), "year", -1, times)).toBeNull();
});
it("does not land back inside the period it started in", () => {
// Both April dives are in the same month, so stepping forward from the
// first must clear the whole month, not advance to its sibling.
expect(stepPeriod(Date.UTC(2025, 3, 10), "month", 1, times)).not.toBe(
Date.UTC(2025, 3, 12),
);
});
});
describe("availablePeriods", () => {
const times = [
Date.UTC(2025, 3, 10),
Date.UTC(2025, 3, 12),
Date.UTC(2025, 9, 5),
Date.UTC(2026, 2, 1),
];
it("lists one option per year that has dives, oldest first", () => {
expect(availablePeriods(times, "year").map((p) => p.start)).toEqual([
Date.UTC(2025, 0, 1),
Date.UTC(2026, 0, 1),
]);
});
it("lists one option per month that has dives", () => {
expect(availablePeriods(times, "month").map((p) => p.start)).toEqual([
Date.UTC(2025, 3, 1),
Date.UTC(2025, 9, 1),
Date.UTC(2026, 2, 1),
]);
});
it("offers no option for a period with no dives", () => {
// 2024 sits between two years that do have dives and must not appear.
const spanning = [Date.UTC(2023, 5, 1), Date.UTC(2025, 5, 1)];
expect(availablePeriods(spanning, "year").map((p) => p.start)).toEqual([
Date.UTC(2023, 0, 1),
Date.UTC(2025, 0, 1),
]);
});
it("carries a real dive time as the anchor, not the period start", () => {
// The whole point: picking "2025" and then switching to Month has to land
// on a month with dives in it, not on an empty January.
const [y2025] = availablePeriods(times, "year");
expect(times).toContain(y2025.anchor);
expect(y2025.anchor).not.toBe(y2025.start);
});
it("represents each period by its most recent dive", () => {
const [april] = availablePeriods(times, "month");
expect(april.anchor).toBe(Date.UTC(2025, 3, 12));
});
it("returns nothing for an empty series", () => {
expect(availablePeriods([], "year")).toEqual([]);
});
});
describe("resolveAnchor", () => {
// Two dives in April 2025, one in October, one in March 2026.
const times = [
Date.UTC(2025, 3, 10),
Date.UTC(2025, 3, 12),
Date.UTC(2025, 9, 5),
Date.UTC(2026, 2, 1),
];
it("keeps an anchor whose dive is still there", () => {
expect(resolveAnchor(times[2], "month", times)).toBe(times[2]);
});
it("falls back to the most recent when nothing was remembered", () => {
expect(resolveAnchor(null, "year", times)).toBeNull();
});
it("falls back to the most recent when there are no dives at all", () => {
expect(resolveAnchor(times[0], "year", [])).toBeNull();
});
it("lands on the same period when the remembered dive is gone", () => {
// The April 10th dive was deleted, or its time was edited. The diver was
// reading April 2025 and should still be reading April 2025.
const withoutFirst = times.slice(1);
expect(resolveAnchor(times[0], "month", withoutFirst)).toBe(times[1]);
});
it("gives up when the whole period is gone", () => {
// Every April dive removed - there is no April 2025 to restore, and the
// period select would render an empty trigger if we tried.
const withoutApril = times.slice(2);
expect(resolveAnchor(times[0], "month", withoutApril)).toBeNull();
});
it("resolves against the period the scope means", () => {
// The same missing dive: April is gone as a *month*, but 2025 still has
// October in it, so the year scope has somewhere to land.
const withoutApril = times.slice(2);
expect(resolveAnchor(times[0], "month", withoutApril)).toBeNull();
expect(resolveAnchor(times[0], "year", withoutApril)).toBe(times[2]);
});
it("has no period to fall back to in the all scope", () => {
// "All" plots everything regardless, so a stale anchor there only affects
// where switching back to Year or Month lands - the default is right.
expect(resolveAnchor(Date.UTC(2020, 0, 1), "all", times)).toBeNull();
});
});