import { describe, expect, it } from "vitest"; import type { TourDefinition } from "@/lib/tours/tour-types"; import * as onboardingPublicApi from "../index"; import type { OnboardingContext, OnboardingFlow } from "../onboarding-types"; import { getFlowById, getOrderedFlows, onboardingFlows } from "../registry"; // Registry only reads { id, version } from tours, so a flat stub is sufficient. const buildTour = (id: string, version = 1): TourDefinition => ({ id, version, coversFiles: [], steps: [], }); const buildFlow = ( overrides: Partial = {}, ): OnboardingFlow => ({ id: overrides.id ?? "flow", order: overrides.order ?? 1, title: overrides.title ?? "Title", description: overrides.description ?? "Description", route: overrides.route ?? "/route", tour: overrides.tour ?? buildTour(overrides.id ?? "flow"), isComplete: overrides.isComplete, ownsAutoOpen: overrides.ownsAutoOpen, }); describe("OnboardingFlow / OnboardingContext types", () => { it("compiles a fully-populated OnboardingFlow against the declared contract", () => { const ctx: OnboardingContext = { hasProviders: false }; const flow: OnboardingFlow = { id: "add-provider", order: 1, title: "Add your first provider", description: "Connect a cloud account to start scanning.", route: "/providers", tour: buildTour("add-provider"), isComplete: (c: OnboardingContext) => c.hasProviders, }; expect(flow.id).toBe("add-provider"); expect(flow.order).toBe(1); expect(flow.route).toBe("/providers"); expect(flow.isComplete?.(ctx)).toBe(false); expect(flow.isComplete?.({ hasProviders: true })).toBe(true); }); }); describe("onboardingFlows (production registry)", () => { it("registers the add-provider flow as the first onboarding flow", () => { const addProvider = getFlowById("add-provider", onboardingFlows); expect(addProvider).toBeDefined(); expect(addProvider?.order).toBe(1); expect(addProvider?.route).toBe("/providers"); expect(addProvider?.tour.id).toBe("add-provider"); }); it("treats add-provider as complete when the context reports providers", () => { const addProvider = getFlowById("add-provider", onboardingFlows); expect(addProvider?.isComplete?.({ hasProviders: true })).toBe(true); expect(addProvider?.isComplete?.({ hasProviders: false })).toBe(false); }); it("registers the view-first-scan flow at order 2 on the active scans tab", () => { const flow = getFlowById("view-first-scan", onboardingFlows); expect(flow).toBeDefined(); expect(flow?.order).toBe(2); expect(flow?.route).toBe("/scans?tab=active"); expect(flow?.tour.id).toBe("view-first-scan"); }); it("registers the explore-findings flow at order 3 on the findings route", () => { const flow = getFlowById("explore-findings", onboardingFlows); expect(flow).toBeDefined(); expect(flow?.order).toBe(3); expect(flow?.route).toBe("/findings"); expect(flow?.tour.id).toBe("explore-findings"); }); it("registers the view-compliance flow at order 4 on the single scan tab", () => { const flow = getFlowById("view-compliance", onboardingFlows); expect(flow).toBeDefined(); expect(flow?.order).toBe(4); expect(flow?.route).toBe("/compliance?tab=per-scan"); expect(flow?.tour.id).toBe("view-compliance"); }); it("registers the attack-paths flow at order 5 on the attack-paths route", () => { const flow = getFlowById("attack-paths", onboardingFlows); expect(flow).toBeDefined(); expect(flow?.order).toBe(5); expect(flow?.route).toBe("/attack-paths"); expect(flow?.tour.id).toBe("attack-paths"); }); it("flags attack-paths with ownsAutoOpen so the shared trigger never drives it", () => { // attack-paths page owns its own driver; all other flows must be falsy const attackPaths = getFlowById("attack-paths", onboardingFlows); expect(attackPaths?.ownsAutoOpen).toBe(true); for (const flow of onboardingFlows) { if (flow.id === "attack-paths") continue; expect(flow.ownsAutoOpen).toBeFalsy(); } }); it("sets a data requirement hint on the scan-dependent flows only", () => { const scanDependent = [ "explore-findings", "view-compliance", "attack-paths", ]; const standalone = ["add-provider", "view-first-scan"]; const expectedHint = "Wait for the scan to finish and have findings."; for (const id of scanDependent) { const flow = getFlowById(id, onboardingFlows); expect(flow?.dataRequirementHint).toBe(expectedHint); } for (const id of standalone) { const flow = getFlowById(id, onboardingFlows); expect(flow?.dataRequirementHint).toBeUndefined(); } }); it("orders the five sequence flows 1..5 by registry order", () => { const ordered = getOrderedFlows(onboardingFlows); expect(ordered.map((flow) => flow.id)).toEqual([ "add-provider", "view-first-scan", "explore-findings", "view-compliance", "attack-paths", ]); }); }); describe("public api barrel", () => { it("re-exports the registry and gate-decision surface", () => { expect(onboardingPublicApi.getOrderedFlows).toBe(getOrderedFlows); expect(onboardingPublicApi.getFlowById).toBe(getFlowById); expect(onboardingPublicApi.onboardingFlows).toBe(onboardingFlows); expect(typeof onboardingPublicApi.shouldStartOnboarding).toBe("function"); }); }); describe("getOrderedFlows", () => { it("returns an empty array when the registry has no flows", () => { expect(getOrderedFlows([])).toEqual([]); }); it("sorts ascending by order, stably preserving array position on ties", () => { const second = buildFlow({ id: "second", order: 2 }); const first = buildFlow({ id: "first", order: 1 }); const secondTie = buildFlow({ id: "second-tie", order: 2 }); const flows = [second, first, secondTie]; // order 1 first, then the two order-2 entries in original sequence expect(getOrderedFlows(flows).map((f) => f.id)).toEqual([ "first", "second", "second-tie", ]); }); }); describe("getFlowById", () => { it("returns the matching flow when the id exists", () => { const addProvider = buildFlow({ id: "add-provider", order: 1 }); const flows = [addProvider, buildFlow({ id: "other", order: 2 })]; expect(getFlowById("add-provider", flows)).toBe(addProvider); }); it("returns undefined when the id is unknown", () => { const flows = [buildFlow({ id: "add-provider", order: 1 })]; expect(getFlowById("unknown-xyz", flows)).toBeUndefined(); }); }); describe("getFlowById fallback", () => { it("returns undefined for an unknown id against the production registry", () => { expect(getFlowById("does-not-exist", onboardingFlows)).toBeUndefined(); }); });