// DashboardPage reads the shared getting-started path (#396). No jsdom: // react-dom/server renders in vitest's node environment. The path comes from a // fake `useOnboarding()`, and the dashboard's data hook and widgets are stubbed // to markers: what is pinned is the page's branching on the path. Since #397 // the page has no list of its own once transactions exist: the banner of // AppShell replaced the compact bar (D7). import { describe, it, expect, vi, beforeEach } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; import { MemoryRouter } from "react-router-dom"; import type { OnboardingValue } from "../hooks/useOnboardingChecklist"; vi.mock("react-i18next", async () => (await import("../test/i18nMock")).i18nMock()); let onboarding: OnboardingValue; vi.mock("../contexts/OnboardingContext", () => ({ useOnboarding: () => onboarding })); vi.mock("../hooks/useDashboard", () => ({ useDashboard: () => ({ state: { categoryBreakdown: [], categoryOverTime: { categories: [], data: [] }, cartesSnapshot: null, period: "year", customDateFrom: "2026-01-01", customDateTo: "2026-10-08", referenceYear: 2026, referenceMonth: 9, accountIds: [], accounts: [], netWorth: null, isLoading: false, error: null, }, setPeriod: vi.fn(), setCustomDates: vi.fn(), setReferencePeriod: vi.fn(), setAccountIds: vi.fn(), }), })); // Hoisted with the mocks below, which run before the imports. const { marker, emptyStateProps } = vi.hoisted(() => ({ marker: (name: string) => ({ default: () => }), emptyStateProps: { current: null as Record | null }, })); vi.mock("../components/shared/PageHelp", () => ({ PageHelp: () => null })); vi.mock("../components/dashboard/CategoriesV1DiscoveryBanner", () => marker("discovery")); vi.mock("../components/dashboard/DashboardEmptyState", () => ({ default: (props: Record) => { emptyStateProps.current = props; return ; }, })); // The page no longer imports the list; the marker would show a compact bar // that came back. vi.mock("../components/dashboard/OnboardingChecklist", () => marker("checklist")); vi.mock("../components/dashboard/PeriodSelector", () => marker("period")); vi.mock("../components/dashboard/NetWorthTile", () => marker("net-worth")); vi.mock("../components/reports/CategoryBarChart", () => marker("bar-chart")); vi.mock("../components/reports/CategoryOverTimeChart", () => marker("over-time")); vi.mock("../components/reports/FilterPanel", () => marker("filters")); vi.mock("../components/reports/cards/KpiCard", () => marker("kpi")); vi.mock("../components/reports/cards/IncomeExpenseOverlayChart", () => marker("overlay")); vi.mock("../components/reports/cards/TopMoversList", () => marker("movers")); vi.mock("../components/reports/cards/BudgetAdherenceCard", () => marker("adherence")); vi.mock("../components/shared/TransactionDetailModal", () => marker("detail")); import DashboardPage from "./DashboardPage"; import { computeChecklist } from "../utils/onboardingChecklist"; import type { OnboardingCounters } from "../shared/types"; const counters = (transactions: number): OnboardingCounters => ({ sources: transactions > 0 ? 1 : 0, imports: transactions > 0 ? 1 : 0, manualCategorized: 0, budgetEntries: 0, transactions, }); function path(overrides: Partial & { transactions?: number } = {}): OnboardingValue { const { transactions = 25, ...rest } = overrides; const c = counters(transactions); return { status: "ready", checklist: computeChecklist(c, { importFolderSet: true, budgetAllowed: true, visitStepsInitialized: true, acknowledged: c.imports >= 1 ? ["categories", "reports"] : [], }), counters: c, hidden: false, budgetReady: true, visitStepsInitialized: true, nextStep: null, mode: "banner", cardPage: null, hide: vi.fn(async () => undefined), show: vi.fn(async () => undefined), skip: vi.fn(async () => undefined), resume: vi.fn(async () => undefined), acknowledge: vi.fn(async () => undefined), refresh: vi.fn(async () => undefined), ...rest, }; } const render = () => renderToStaticMarkup( , ); beforeEach(() => { onboarding = path(); emptyStateProps.current = null; }); describe("DashboardPage on the shared path (#352, #396, #397)", () => { it("renders only its header while the path first loads", () => { onboarding = path({ status: "loading", checklist: null, counters: null }); const html = render(); expect(html).toContain("t(dashboard.title)"); expect(html).not.toContain('data-marker="empty-state"'); expect(html).not.toContain('data-marker="filters"'); }); it("shows the welcome card on a profile without any transaction", () => { onboarding = path({ transactions: 0 }); const html = render(); expect(html).toContain('data-marker="empty-state"'); expect(html).not.toContain('data-marker="filters"'); }); it("wires the welcome card's list to the path: hidden state, 'Hide' and 'Resume'", () => { onboarding = path({ transactions: 0, hidden: true }); render(); const props = emptyStateProps.current!; expect(props.checklist).toBe(onboarding.checklist); expect(props.hidden).toBe(true); expect(props.budgetReady).toBe(true); expect(props.onHide).toBe(onboarding.hide); expect(props.onResume).toBe(onboarding.resume); }); it.each([ ["shown", false], ["hidden", true], ])("has no list of its own on a profile with transactions, path %s: the banner replaced the compact bar (D7)", (_, hidden) => { onboarding = path({ hidden, mode: hidden ? "none" : "banner" }); const html = render(); expect(html).not.toContain('data-marker="checklist"'); expect(html).not.toContain('data-marker="empty-state"'); expect(html).toContain('data-marker="filters"'); }); it("renders the page without the list when the first load failed", () => { onboarding = path({ status: "error", checklist: null, counters: null, mode: "none" }); const html = render(); expect(html).not.toContain('data-marker="checklist"'); expect(html).not.toContain('data-marker="empty-state"'); expect(html).toContain('data-marker="filters"'); }); });