import { beforeEach, describe, expect, it } from "vitest"; import { DEFAULT_GATES, visibleTourSteps, type TourGateContext, type TourStep, } from "@/lib/tours/types"; import { TOURS, getTourForPath } from "@/lib/tours/registry"; import { isTourDismissed, isToursOptedOut, readTourDismissals, setToursOptedOut, storeTourDismissal, writeTourDismissals, } from "@/lib/tours/dismissals"; const gates = (overrides: Partial): TourGateContext => ({ ...DEFAULT_GATES, ...overrides, }); describe("tour step filtering", () => { const steps: TourStep[] = [ { id: "open", selector: '[data-tour="page-header"]', title: "Open", body: "b" }, { id: "logistics-only", selector: '[data-tour="logistics-only"]', title: "Logistics", body: "b", showWhen: (g) => g.hasLogistics, }, { id: "intel-only", selector: '[data-tour="intel-only"]', title: "Intel", body: "b", showWhen: (g) => g.hasIntelligence, }, { id: "stats-flag", selector: '[data-tour="stats"]', title: "Stats", body: "b", showWhen: (g) => g.statisticsEnabled, }, ]; it("a fully privileged visitor sees every step", () => { expect(visibleTourSteps(steps, DEFAULT_GATES)).toHaveLength(4); }); it("a visitor without logistics qualification never sees logistics steps", () => { const visible = visibleTourSteps(steps, gates({ hasLogistics: false })); expect(visible.map((s) => s.id)).toEqual(["open", "intel-only", "stats-flag"]); }); it("a visitor without intelligence qualification never sees intel steps", () => { const visible = visibleTourSteps(steps, gates({ hasIntelligence: false })); expect(visible.map((s) => s.id)).toEqual(["open", "logistics-only", "stats-flag"]); }); it("feature-flag gates hide steps when the feature is disabled", () => { const visible = visibleTourSteps(steps, gates({ statisticsEnabled: false })); expect(visible.map((s) => s.id)).toEqual(["open", "logistics-only", "intel-only"]); }); it("a visitor with nothing unlocked sees only ungated steps", () => { const visible = visibleTourSteps( steps, gates({ hasLogistics: false, hasIntelligence: false, hasAdminPanel: false, statisticsEnabled: false, helpdeskEnabled: false, }), ); expect(visible.map((s) => s.id)).toEqual(["open"]); }); }); describe("tour registry", () => { it("covers the major sidebar routes", () => { const paths = TOURS.map((tour) => tour.path); for (const path of [ "/", "/roster", "/map", "/operations", "/logistics", "/logistics/shipments", "/logistics/banking", "/logistics/market", "/wiki", "/awards", "/locker", ]) { expect(paths, `missing tour for ${path}`).toContain(path); } }); it("has unique tour ids and unique paths", () => { expect(new Set(TOURS.map((t) => t.id)).size).toBe(TOURS.length); expect(new Set(TOURS.map((t) => t.path)).size).toBe(TOURS.length); }); it("gives every step a unique id and a data-tour anchor", () => { for (const tour of TOURS) { expect(tour.steps.length, `${tour.id} has steps`).toBeGreaterThan(0); expect(new Set(tour.steps.map((s) => s.id)).size, tour.id).toBe(tour.steps.length); for (const step of tour.steps) { expect(step.selector, `${tour.id}/${step.id}`).toMatch(/^(\[data-tour=|html|body|#)/); expect(step.body.length, `${tour.id}/${step.id}`).toBeGreaterThan(0); expect(step.body, `${tour.id}/${step.id} copy is dash-clean`).not.toMatch(/[–—]/); } } }); it("looks up tours by exact path and ignores query strings", () => { expect(getTourForPath("/")?.id).toBe("dashboard"); expect(getTourForPath("/logistics/shipments")?.id).toBe("shipments"); expect(getTourForPath("/logistics/shipments?foo=bar")).toBeNull(); expect(getTourForPath("/no/such/page")).toBeNull(); expect(getTourForPath(null)).toBeNull(); expect(getTourForPath(undefined)).toBeNull(); }); }); describe("tour dismissals (per-browser localStorage)", () => { const originalSetItem = window.localStorage.setItem.bind(window.localStorage); const originalGetItem = window.localStorage.getItem.bind(window.localStorage); beforeEach(() => { window.localStorage.clear(); }); it("stores, reads, and reports dismissals", () => { const now = 1_000_000; expect(isTourDismissed("dashboard", now)).toBe(false); storeTourDismissal("dashboard", now); expect(isTourDismissed("dashboard", now)).toBe(true); expect(isTourDismissed("other-tour", now)).toBe(false); expect(readTourDismissals(now)).toEqual({ dashboard: now }); }); it("merges dismissals without losing other tours", () => { const now = 5_000; window.localStorage.clear(); writeTourDismissals({ a: now }, now); storeTourDismissal("b", now + 1); expect(readTourDismissals(now)).toEqual({ a: now, b: now + 1 }); }); it("degrades to an empty map when storage reads fail", () => { window.localStorage.setItem = () => { throw new Error("disabled"); }; window.localStorage.getItem = () => { throw new Error("disabled"); }; expect(readTourDismissals(1)).toEqual({}); expect(isTourDismissed("dashboard", 1)).toBe(false); // Writers swallow the failure instead of crashing the page. expect(() => storeTourDismissal("dashboard", 1)).not.toThrow(); }); window.localStorage.setItem = originalSetItem; window.localStorage.getItem = originalGetItem; }); describe("tours opt-out (skip all)", () => { beforeEach(() => { window.localStorage.clear(); }); it("defaults to opted-in and toggles per browser", () => { expect(isToursOptedOut()).toBe(false); setToursOptedOut(true); expect(isToursOptedOut()).toBe(true); setToursOptedOut(false); expect(isToursOptedOut()).toBe(false); }); it("keeps the opt-out independent of per-tour dismissals", () => { setToursOptedOut(true); storeTourDismissal("dashboard", 1); setToursOptedOut(false); expect(isTourDismissed("dashboard", 1)).toBe(true); expect(isToursOptedOut()).toBe(false); }); });