src/lib/course.test.ts
Rama main · réplica de lectura del repositorio original.
import { describe, expect, it } from "vitest";
import { COURSE_STATUSES, type Course } from "@/lib/api/courses";
import {
courseStatusBadgeVariant,
courseStatusLabel,
courseVocabulary,
} from "@/lib/course";
describe("courseStatusLabel", () => {
it("names every status the API can send", () => {
// Driven off the vocabulary itself rather than a second hand-written list,
// so a status added to `COURSE_STATUSES` without a label fails here.
const labels = COURSE_STATUSES.map(courseStatusLabel);
expect(labels).toEqual([
"Planned",
"In progress",
"Completed",
"Incomplete",
"Provisional",
"Not passed",
]);
});
it("gives each status a label of its own", () => {
const labels = COURSE_STATUSES.map(courseStatusLabel);
expect(new Set(labels).size).toBe(COURSE_STATUSES.length);
});
it("de-snakes a status this build doesn't know", () => {
expect(courseStatusLabel("on_hold")).toBe("On hold");
});
it("renders nothing for a missing status", () => {
expect(courseStatusLabel(null)).toBe("");
expect(courseStatusLabel(undefined)).toBe("");
});
});
describe("courseStatusBadgeVariant", () => {
it("gives `completed` a variant no other status wears", () => {
// The one distinction that has to hold: an outcome a diver scans for cannot
// share a badge with the states that mean the course is still open.
const completed = courseStatusBadgeVariant("completed");
const others = COURSE_STATUSES.filter(
(status) => status !== "completed",
).map(courseStatusBadgeVariant);
expect(others).not.toContain(completed);
});
it("badges a failed course destructively", () => {
expect(courseStatusBadgeVariant("not_passed")).toBe("destructive");
});
it("leaves a course under way neutral, never a fill a list card's grey hides", () => {
expect(courseStatusBadgeVariant("in_progress")).toBe("outline");
expect(courseStatusBadgeVariant("planned")).toBe("outline");
});
it("falls back to a neutral variant for an unknown status", () => {
expect(courseStatusBadgeVariant("deferred")).toBe("outline");
expect(courseStatusBadgeVariant(null)).toBe("outline");
});
});
describe("courseVocabulary", () => {
const course = (fields: Partial<Course>): Course =>
({
uuid: crypto.randomUUID(),
name: "Nitrox",
status: "completed",
user_uuid: "diver",
created_at: "2025-01-01T00:00:00Z",
...fields,
}) as Course;
it("offers only the agencies the courses carry", () => {
const { agencies } = courseVocabulary([
course({ agency: "sdi" }),
course({ agency: "padi" }),
course({ agency: "padi" }),
]);
expect(agencies).toEqual(["padi", "sdi"]);
});
it("orders both lists canonically rather than by first appearance", () => {
// `padi` is declared before `sdi`, and `planned` before `completed`, so what
// the diver logged first does not decide what the picker offers first.
const { agencies, statuses } = courseVocabulary([
course({ agency: "sdi", status: "completed" }),
course({ agency: "padi", status: "planned" }),
]);
expect(agencies).toEqual(["padi", "sdi"]);
expect(statuses).toEqual(["planned", "completed"]);
});
it("passes over a course that names no agency", () => {
const { agencies, statuses } = courseVocabulary([
course({ agency: null, status: "in_progress" }),
]);
expect(agencies).toEqual([]);
expect(statuses).toEqual(["in_progress"]);
});
// The same tolerance `courseStatusLabel` has: the API can grow a member before
// this build ships a label for it, and a course wearing one is still a course
// the diver may want to filter to.
it("keeps a status this build does not know, after the ones it does", () => {
const { statuses } = courseVocabulary([
course({ status: "deferred" as Course["status"] }),
course({ status: "completed" }),
]);
expect(statuses).toEqual(["completed", "deferred"]);
});
it("answers an empty logbook with empty lists", () => {
expect(courseVocabulary([])).toEqual({ agencies: [], statuses: [] });
});
});