diff --git a/src/app/(app)/collection-map/page.test.ts b/src/app/(app)/collection-map/page.test.ts index 66703b1..981f0de 100644 --- a/src/app/(app)/collection-map/page.test.ts +++ b/src/app/(app)/collection-map/page.test.ts @@ -92,6 +92,11 @@ describe("collection-map toolbar shape contract", () => { expect(src).not.toMatch(/setReadyFilter|readyFilter/); expect(src).not.toMatch(/Search PO, supplier/); expect(src).not.toMatch(/>\s*Drivers\s* { @@ -102,3 +107,87 @@ describe("collection-map toolbar shape contract", () => { expect(src).not.toMatch(/assigned_at <= "\$\{date\}T23:59:59\.999Z"/); }); }); + +describe("collection-map v3 sidebar contract", () => { + // The sidebar split during Task 3 means: page.tsx owns the *composition* + // (Legend → DriverCardSection → UnassignedCompactCard → MissingCoordsCard) + // and the assign-dialog wire-up. sidebar.tsx owns the inner subcomponents + // (DriverCard, CountsStrip, StopRow, LegendCard, MissingCoordsCard, + // UnassignedCompactCard). We snapshot both files so the assertions + // target the right module. + const pageSrc = fs.readFileSync(path.resolve(__dirname, "./page.tsx"), "utf8"); + const sidebarSrc = fs.readFileSync(path.resolve(__dirname, "./sidebar.tsx"), "utf8"); + + it("imports aggregateDriverStatus from pin-model", () => { + expect(pageSrc).toMatch(/aggregateDriverStatus[\s\S]*?from\s+["']\.\/pin-model["']/); + }); + + it("renders the sidebar in the order: Legend → Driver cards → Unassigned → Missing coords", () => { + const legendIdx = pageSrc.indexOf(" { + expect(sidebarSrc).toMatch(/driversWithItems\.slice\(0,\s*3\)/); + expect(sidebarSrc).toMatch(/Show all \(/); + }); + + it("hides the UnassignedCompactCard when there are no unassigned items", () => { + // Gate is the `> 0` filter — assert it shows up in page.tsx (the gate lives there). + expect(pageSrc).toMatch(/items\.filter\(\(it\)\s*=>\s*!it\.drv_id\)\.length\s*>\s*0/); + // Copy text lives in sidebar.tsx. + expect(sidebarSrc).toMatch(/unassigned stop/); + }); + + it("uses green pill classes for collected per-stop rows", () => { + expect(sidebarSrc).toMatch(/bg-emerald-100 text-emerald-900 border-emerald-300/); + }); + + it("uses amber pill classes for waiting per-stop rows", () => { + expect(sidebarSrc).toMatch(/bg-amber-100 text-amber-900 border-amber-300/); + }); + + it("uses blue pill classes for ready per-stop rows", () => { + expect(sidebarSrc).toMatch(/bg-blue-100 text-blue-900 border-blue-300/); + }); + + it("renders the popup 'X of Y collected' line", () => { + expect(pageSrc).toMatch(/\bcollected\b[^<]*of[^<]*collected/); // "X of Y collected" + expect(pageSrc).toMatch(/text-emerald-700/); // popup emerald color + }); + + it("does NOT add a green Leaflet pin variant", () => { + // The two-pin model stays intact: blue letter + amber ✓/✕ only. + expect(pageSrc).not.toMatch(/driverLetterPin.*emerald|emerald.*driverLetterPin/); + expect(sidebarSrc).not.toMatch(/driverLetterPin.*emerald|emerald.*driverLetterPin/); + // No new pin factory introducing a third color. + expect(pageSrc).not.toMatch(/emerald-(?:500|600|700)\b.*pin|pin.*emerald-(?:500|600|700)\b/); + expect(sidebarSrc).not.toMatch(/emerald-(?:500|600|700)\b.*pin|pin.*emerald-(?:500|600|700)\b/); + }); + + it("uses the existing driversWithItems ordering without re-sorting", () => { + // driverLabelFor is alphabetical; the top 3 cap does not re-sort. + expect(sidebarSrc).toMatch(/driverLabelFor\(driversWithItems,/); + }); + + it("uses the existing openAssignDialog for sidebar row clicks", () => { + expect(pageSrc).toMatch(/onOpenAssign=\{openAssignDialog\}/); + }); + + it("preserves the assign-dialog mount with phase-2 props", () => { + expect(pageSrc).toMatch(/requireReadyForCollection/); + expect(pageSrc).toMatch(/readyForCollection=\{assignDialog\.row\?\.ready_for_collection\s*\?\?\s*false\}/); + }); + + it("preserves the existing toolbar (Refresh + Traffic + Fullscreen)", () => { + expect(pageSrc).toMatch(/Refresh/); + expect(pageSrc).toMatch(/Traffic/); + expect(pageSrc).toMatch(/Fullscreen|Exit fullscreen|Minimize2/); + }); +});