Ctrl K
ComunidadGuías…

src/lib/chart-period.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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();
  });
});