import { describe, expect, it } from "vitest"; import { driverLabelFor, unassignedGlyphFor, jitterCoordinates, distanceKm } from "./geo"; describe("driverLabelFor", () => { it("returns A for the first driver alphabetically", () => { const drivers = [{ id: "u-1" }, { id: "u-2" }].sort((a, b) => a.id.localeCompare(b.id) ); // u-1 < u-2 lexically, so driver at index 0 is "u-1" expect(driverLabelFor(drivers, drivers[0].id)).toBe("A"); expect(driverLabelFor(drivers, drivers[1].id)).toBe("B"); }); it("returns sequential letters A through Z for the first 26 drivers", () => { const drivers = Array.from({ length: 26 }, (_, i) => ({ id: `driver-${String(i).padStart(2, "0")}`, })); const sorted = [...drivers].sort((a, b) => a.id.localeCompare(b.id)); for (let i = 0; i < 26; i++) { expect(driverLabelFor(sorted, sorted[i].id)).toBe( String.fromCharCode("A".charCodeAt(0) + i) ); } }); it("falls back to digits 1..9 starting at position 27", () => { const drivers = Array.from({ length: 30 }, (_, i) => ({ id: `driver-${String(i).padStart(2, "0")}`, })); const sorted = [...drivers].sort((a, b) => a.id.localeCompare(b.id)); expect(driverLabelFor(sorted, sorted[26].id)).toBe("1"); expect(driverLabelFor(sorted, sorted[27].id)).toBe("2"); expect(driverLabelFor(sorted, sorted[28].id)).toBe("3"); expect(driverLabelFor(sorted, sorted[29].id)).toBe("4"); }); it("clamps at '9' for the 35th driver and beyond (no '0' wrap)", () => { const drivers = Array.from({ length: 40 }, (_, i) => ({ id: `driver-${String(i).padStart(2, "0")}`, })); const sorted = [...drivers].sort((a, b) => a.id.localeCompare(b.id)); // Sorted: driver-00 (A) ... driver-34 (9), then driver-35 clamps to "9". expect(driverLabelFor(sorted, sorted[34].id)).toBe("9"); expect(driverLabelFor(sorted, sorted[35].id)).toBe("9"); expect(driverLabelFor(sorted, sorted[39].id)).toBe("9"); }); it("returns '?' for an unknown driver id", () => { const drivers = [{ id: "u-1" }]; expect(driverLabelFor(drivers, "u-missing")).toBe("?"); }); it("is stable — same input returns the same label on repeated calls", () => { const drivers = [{ id: "u-1" }, { id: "u-2" }, { id: "u-3" }]; const sorted = [...drivers].sort((a, b) => a.id.localeCompare(b.id)); const first = driverLabelFor(sorted, "u-2"); const second = driverLabelFor(sorted, "u-2"); const third = driverLabelFor(sorted, "u-2"); expect(first).toBe(second); expect(second).toBe(third); }); it("is stable when given names instead of ids (alphabetical by name)", () => { const drivers = [ { id: "alice" }, { id: "bob" }, { id: "carol" }, ]; expect(driverLabelFor(drivers, "alice")).toBe("A"); expect(driverLabelFor(drivers, "bob")).toBe("B"); expect(driverLabelFor(drivers, "carol")).toBe("C"); }); }); describe("unassignedGlyphFor", () => { it("returns 'ready' when every item is ready_for_collection=true", () => { expect( unassignedGlyphFor([ { ready_for_collection: true }, { ready_for_collection: true }, ]) ).toBe("ready"); }); it("returns 'unready' when at least one item is not ready", () => { expect( unassignedGlyphFor([ { ready_for_collection: true }, { ready_for_collection: false }, ]) ).toBe("unready"); }); it("returns 'unready' when at least one item has ready_for_collection=undefined", () => { expect( unassignedGlyphFor([ { ready_for_collection: true }, {}, ]) ).toBe("unready"); }); it("returns 'unready' for an empty array", () => { expect(unassignedGlyphFor([])).toBe("unready"); }); }); describe("jitterCoordinates", () => { const CENTER: readonly [number, number] = [-26.4, 27.4]; it("offsets the point by a distance within the requested range", () => { // distanceKm is the same great-circle maths the check-in radius // match uses, so this asserts the jitter is bounded in the units // that actually matter for the feature. for (let i = 0; i < 50; i++) { const jittered = jitterCoordinates(CENTER, 30, 80); const metres = distanceKm(CENTER, jittered) * 1000; expect(metres).toBeGreaterThanOrEqual(30 - 0.5); expect(metres).toBeLessThanOrEqual(80 + 0.5); } }); it("never returns the exact same point", () => { expect(jitterCoordinates(CENTER, 30, 80)).not.toEqual(CENTER); }); it("produces different points on repeated calls", () => { // Not a hard guarantee (it's random), but with a continuous range // and a bearing over the full circle, two calls landing on the // exact same float is negligible — this catches an accidental // deterministic implementation. const a = jitterCoordinates(CENTER, 30, 80); const b = jitterCoordinates(CENTER, 30, 80); expect(a).not.toEqual(b); }); it("keeps a zero-width range at that exact distance", () => { const jittered = jitterCoordinates(CENTER, 50, 50); const metres = distanceKm(CENTER, jittered) * 1000; expect(metres).toBeCloseTo(50, 0); }); });