import { describe, it, expect } from "vitest"; import { computeChecklist } from "./onboardingChecklist"; import type { ChecklistItemKey, OnboardingCounters } from "../shared/types"; const zero: OnboardingCounters = { sources: 0, imports: 0, manualCategorized: 0, budgetEntries: 0, transactions: 0, }; const open = { importFolderSet: false, budgetAllowed: true }; function keys(counters: OnboardingCounters, opts = open): ChecklistItemKey[] { return computeChecklist(counters, opts).items.map((i) => i.key); } function doneKeys(counters: OnboardingCounters, opts = open): ChecklistItemKey[] { return computeChecklist(counters, opts) .items.filter((i) => i.done) .map((i) => i.key); } describe("computeChecklist — rows (#351)", () => { it("lists the six rows in order", () => { expect(keys(zero)).toEqual([ "profile", "importFolder", "source", "import", "manualCategory", "budget", ]); }); it("zero counters → only profile is done (1/6)", () => { const list = computeChecklist(zero, open); expect(doneKeys(zero)).toEqual(["profile"]); expect(list.done).toBe(1); expect(list.total).toBe(6); expect(list.complete).toBe(false); }); it("import folder chosen without any source → done=2", () => { const list = computeChecklist(zero, { ...open, importFolderSet: true }); expect(doneKeys(zero, { ...open, importFolderSet: true })).toEqual([ "profile", "importFolder", ]); expect(list.done).toBe(2); }); it("source row follows sources ≥ 1", () => { expect(doneKeys({ ...zero, sources: 1 })).toContain("source"); expect(doneKeys({ ...zero, sources: 1 })).not.toContain("import"); }); it("import row follows imports ≥ 1", () => { expect(doneKeys({ ...zero, imports: 1 })).toContain("import"); expect(doneKeys({ ...zero, imports: 1 })).not.toContain("source"); }); it("manualCategory row follows manualCategorized ≥ 1 (not transactions)", () => { expect(doneKeys({ ...zero, transactions: 50 })).not.toContain("manualCategory"); expect(doneKeys({ ...zero, manualCategorized: 1 })).toContain("manualCategory"); }); it("budget row follows budgetEntries ≥ 1", () => { expect(doneKeys({ ...zero, budgetEntries: 1 })).toContain("budget"); }); it("links each row to its page", () => { const to = Object.fromEntries( computeChecklist(zero, open).items.map((i) => [i.key, i.to]), ); expect(to).toEqual({ profile: "/settings/users", importFolder: "/import", source: "/import", import: "/import", manualCategory: "/transactions", budget: "/budget", }); }); it("no row is locked when Budget is allowed", () => { expect(computeChecklist(zero, open).items.every((i) => !i.locked)).toBe(true); }); }); describe("computeChecklist — budget gating (#351)", () => { const locked = { importFolderSet: false, budgetAllowed: false }; it("budgetAllowed=false → total=5 and the budget row is locked", () => { const list = computeChecklist(zero, locked); expect(list.total).toBe(5); const budget = list.items.find((i) => i.key === "budget")!; expect(budget.locked).toBe(true); expect(budget.to).toBe("/settings/users"); expect(list.items.filter((i) => i.locked)).toHaveLength(1); }); it("a locked budget row never counts as done, even with entries", () => { const list = computeChecklist({ ...zero, budgetEntries: 3 }, locked); expect(list.done).toBe(1); }); it("complete is true at 5/5 when Budget is locked", () => { const all = { ...zero, sources: 1, imports: 1, manualCategorized: 1 }; const list = computeChecklist(all, { ...locked, importFolderSet: true }); expect(list.done).toBe(5); expect(list.total).toBe(5); expect(list.complete).toBe(true); }); it("complete is false at 5/6 when Budget is open but empty", () => { const all = { ...zero, sources: 1, imports: 1, manualCategorized: 1 }; const list = computeChecklist(all, { ...open, importFolderSet: true }); expect(list.done).toBe(5); expect(list.total).toBe(6); expect(list.complete).toBe(false); }); it("complete is true at 6/6 when Budget is open", () => { const all = { ...zero, sources: 1, imports: 1, manualCategorized: 1, budgetEntries: 1 }; const list = computeChecklist(all, { ...open, importFolderSet: true }); expect(list.done).toBe(6); expect(list.total).toBe(6); expect(list.complete).toBe(true); }); }); describe("computeChecklist — skipped steps (#396, D3)", () => { const fiveDone = { ...zero, sources: 1, imports: 1, manualCategorized: 1 }; it("no row is skipped when the option is omitted or empty", () => { expect(computeChecklist(zero, open).items.every((i) => !i.skipped)).toBe(true); expect(computeChecklist(zero, { ...open, skipped: [] }).items.every((i) => !i.skipped)).toBe( true, ); }); it("a skipped row is marked skipped, not done, and counts in the counter", () => { const list = computeChecklist(zero, { ...open, skipped: ["importFolder"] }); const row = list.items.find((i) => i.key === "importFolder")!; expect(row.skipped).toBe(true); expect(row.done).toBe(false); expect(list.done).toBe(2); // profile + the skipped row expect(list.total).toBe(6); }); it("complete with done and skipped rows", () => { const list = computeChecklist(fiveDone, { ...open, importFolderSet: true, skipped: ["budget"] }); expect(list.done).toBe(6); expect(list.total).toBe(6); expect(list.complete).toBe(true); }); it("a locked row stays out of the total, even skipped", () => { const list = computeChecklist(zero, { importFolderSet: false, budgetAllowed: false, skipped: ["budget"] }); const budget = list.items.find((i) => i.key === "budget")!; expect(budget.locked).toBe(true); expect(list.total).toBe(5); expect(list.done).toBe(1); expect(list.complete).toBe(false); }); it("a row both done and skipped counts once", () => { const list = computeChecklist(fiveDone, { ...open, importFolderSet: true, skipped: ["source", "import", "budget"], }); const source = list.items.find((i) => i.key === "source")!; expect(source.done).toBe(true); expect(source.skipped).toBe(true); expect(list.done).toBe(6); expect(list.total).toBe(6); }); it("skipping does not change the rows' links", () => { const to = computeChecklist(zero, { ...open, skipped: ["manualCategory", "budget"] }).items.map( (i) => i.to, ); expect(to).toEqual(computeChecklist(zero, open).items.map((i) => i.to)); }); });