src/lib/dive-numbering.test.ts
Rama main · réplica de lectura del repositorio original.
import { describe, expect, it } from "vitest";
import type { DiveNumberingSummary } from "@/lib/api/dives";
import type { LogbookImportDive } from "@/lib/api/logbook-import";
import {
describeDiveNumbering,
importRenumberScope,
numberingTangledSince,
} from "@/lib/dive-numbering";
function summary(
overrides: Partial<DiveNumberingSummary> = {},
): DiveNumberingSummary {
return {
total_dives: 3,
lowest: 1,
highest: 3,
missing_count: 0,
duplicate_count: 0,
out_of_date_order_count: 0,
is_sequential: true,
...overrides,
};
}
describe("describeDiveNumbering", () => {
it("says nothing about an empty log", () => {
expect(
describeDiveNumbering(
summary({ total_dives: 0, lowest: null, highest: null }),
),
).toBeNull();
});
it("says nothing about a log numbered consecutively in date order", () => {
expect(describeDiveNumbering(summary())).toBeNull();
});
it("says nothing about a clean log that starts above one", () => {
// The first 46 dives are in a paper logbook. Nothing is wrong here, and a
// card offering to renumber would imply otherwise.
expect(
describeDiveNumbering(summary({ lowest: 47, highest: 49 })),
).toBeNull();
});
it("says nothing about a single dive, which is always tidy", () => {
expect(
describeDiveNumbering(
summary({ total_dives: 1, lowest: 12, highest: 12 }),
),
).toBeNull();
});
it("gives a log collapsed onto one number that number rather than a range", () => {
// Three dives all carrying #12: the range has no width, but there is still
// something for a renumber to do.
expect(
describeDiveNumbering(
summary({ lowest: 12, highest: 12, duplicate_count: 2 }),
),
).toBe("Numbered #12 — 2 dives share a number.");
});
it("counts unused numbers", () => {
expect(
describeDiveNumbering(
summary({ highest: 6, missing_count: 3, is_sequential: false }),
),
).toBe("Numbered #1–#6 — 3 numbers unused.");
});
it("counts a single unused number in the singular", () => {
expect(
describeDiveNumbering(
summary({ highest: 4, missing_count: 1, is_sequential: false }),
),
).toBe("Numbered #1–#4 — 1 number unused.");
});
it("counts shared numbers", () => {
expect(describeDiveNumbering(summary({ duplicate_count: 2 }))).toBe(
"Numbered #1–#3 — 2 dives share a number.",
);
});
it("counts a single shared number in the singular", () => {
expect(describeDiveNumbering(summary({ duplicate_count: 1 }))).toBe(
"Numbered #1–#3 — 1 dive shares a number.",
);
});
it("lists everything that's off in one sentence", () => {
expect(
describeDiveNumbering(
summary({
total_dives: 10,
highest: 20,
missing_count: 11,
duplicate_count: 2,
out_of_date_order_count: 4,
is_sequential: false,
}),
),
).toBe(
"Numbered #1–#20 — 11 numbers unused, 2 dives share a number, 4 out of date order.",
);
});
it("reports dives numbered out of date order even when the run is unbroken", () => {
// 1, 3, 2 uses every number once between 1 and 3, so nothing is missing or
// duplicated - but the log still doesn't read in the order it was dived.
expect(describeDiveNumbering(summary({ out_of_date_order_count: 1 }))).toBe(
"Numbered #1–#3 — 1 out of date order.",
);
});
});
describe("numberingTangledSince", () => {
it("is true where more dives share a number than before", () => {
expect(
numberingTangledSince(summary(), summary({ duplicate_count: 2 })),
).toBe(true);
});
it("is true where more dives are out of date order than before", () => {
expect(
numberingTangledSince(
summary({ out_of_date_order_count: 1 }),
summary({ out_of_date_order_count: 3 }),
),
).toBe(true);
});
it("is false where the log is no more tangled than before", () => {
const tangled = summary({ duplicate_count: 2, out_of_date_order_count: 1 });
expect(numberingTangledSince(tangled, tangled)).toBe(false);
});
it("is false where only unused numbers grew, which a file's own numbers make", () => {
expect(
numberingTangledSince(summary(), summary({ missing_count: 40 })),
).toBe(false);
});
});
function importedDive(
start_time: string | null,
outcome: LogbookImportDive["outcome"] = "new",
): Pick<LogbookImportDive, "outcome" | "start_time"> {
return { outcome, start_time };
}
describe("importRenumberScope", () => {
it("starts on the day of the earliest dive the import brought into the log", () => {
expect(
importRenumberScope(
[
importedDive("2019-05-01T09:00:00+02:00"),
importedDive("2019-03-12T09:00:00+02:00"),
// Already in the log, so not where the import's dives begin.
importedDive("2018-01-01T09:00:00+02:00", "unchanged"),
importedDive("2017-01-01T09:00:00+02:00", "modified"),
importedDive("2016-01-01T09:00:00+02:00", "skipped"),
],
120,
),
).toEqual({
fromDate: "2019-03-12",
lastInstantBefore: "2019-03-11T23:59:59+02:00",
});
});
it("is null where no dive entered the log", () => {
expect(
importRenumberScope(
[
importedDive("2019-03-12T09:00:00+02:00", "unchanged"),
importedDive(null, "skipped"),
],
120,
),
).toBeNull();
});
it("reads the day at the given offset, so the scope reaches a dive just past midnight further east", () => {
// 19:30 on the 11th in UTC, 22:30 at +03:00.
expect(
importRenumberScope([importedDive("2019-03-12T00:30:00+05:00")], 180),
).toEqual({
fromDate: "2019-03-11",
lastInstantBefore: "2019-03-10T23:59:59+03:00",
});
});
it("places a naive start at its wall clock as UTC and a bare date at the start of its day", () => {
expect(
importRenumberScope([importedDive("2019-03-12T01:00:00")], -120),
).toEqual({
fromDate: "2019-03-11",
lastInstantBefore: "2019-03-10T23:59:59-02:00",
});
expect(importRenumberScope([importedDive("2019-03-12")], 0)).toEqual({
fromDate: "2019-03-12",
lastInstantBefore: "2019-03-11T23:59:59+00:00",
});
});
});