docs(onboarding): user guide, architecture and QA checklist for the guided getting-started path #407
3 changed files with 50 additions and 6 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
// subtotal-position preference is read from a stubbed localStorage. What is
|
// subtotal-position preference is read from a stubbed localStorage. What is
|
||||||
// pinned is where the one highlight lands: "Expand all" or this month's cell
|
// pinned is where the one highlight lands: "Expand all" or this month's cell
|
||||||
// of the first editable row shown.
|
// of the first editable row shown.
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import type { BudgetYearRow } from "../../shared/types";
|
import type { BudgetYearRow } from "../../shared/types";
|
||||||
import type { CollapseAccessors } from "../../utils/collapsibleRows";
|
import type { CollapseAccessors } from "../../utils/collapsibleRows";
|
||||||
|
|
@ -172,10 +172,24 @@ describe("BudgetTable — the getting-started card's target (#400)", () => {
|
||||||
expect(count(html, HIGHLIGHT)).toBe(1);
|
expect(count(html, HIGHLIGHT)).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("in a timezone west of Greenwich", () => {
|
||||||
|
// Under TZ=UTC (the CI runner), 23:00 on December 31 is December both in
|
||||||
|
// local time and in UTC, so a month read in UTC would pass unnoticed. The
|
||||||
|
// zone is pinned as in useImportWizard.test.ts (#356).
|
||||||
|
const originalTz = process.env.TZ;
|
||||||
|
beforeAll(() => {
|
||||||
|
process.env.TZ = "America/Toronto";
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
if (originalTz === undefined) delete process.env.TZ;
|
||||||
|
else process.env.TZ = originalTz;
|
||||||
|
});
|
||||||
|
|
||||||
it("the cell follows the clock's month", () => {
|
it("the cell follows the clock's month", () => {
|
||||||
vi.setSystemTime(new Date(2026, DECEMBER, 31, 23, 0));
|
vi.setSystemTime(new Date(2026, DECEMBER, 31, 23, 0));
|
||||||
expect(ringedMonth(rowOf(render(FLAT, true), "Divers"))).toBe(DECEMBER);
|
expect(ringedMonth(rowOf(render(FLAT, true), "Divers"))).toBe(DECEMBER);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("subtotals at the bottom: still the first editable row shown", () => {
|
it("subtotals at the bottom: still the first editable row shown", () => {
|
||||||
vi.stubGlobal("localStorage", { getItem: () => "bottom", setItem: () => undefined });
|
vi.stubGlobal("localStorage", { getItem: () => "bottom", setItem: () => undefined });
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,13 @@ export default function BudgetPage() {
|
||||||
|
|
||||||
// The getting-started card follows the grid (#400). Its target is
|
// The getting-started card follows the grid (#400). Its target is
|
||||||
// highlighted only while the card is on screen: a ring needs the card that
|
// highlighted only while the card is on screen: a ring needs the card that
|
||||||
// explains it.
|
// explains it. The grid's last report does not outlive the grid: the rows
|
||||||
const cardModel = budgetCardModel({ target: gridTarget }, onboarding.checklist);
|
// can land before the transaction count (both reads queue behind the DB
|
||||||
|
// lock), so the grid may report, then give way to the empty state.
|
||||||
|
const cardModel = budgetCardModel(
|
||||||
|
{ target: isEmptyProfile ? null : gridTarget },
|
||||||
|
onboarding.checklist,
|
||||||
|
);
|
||||||
const highlight =
|
const highlight =
|
||||||
cardModel.target !== null && showsStepCard(onboarding.mode, onboarding.cardPage, pathname);
|
cardModel.target !== null && showsStepCard(onboarding.mode, onboarding.cardPage, pathname);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,4 +183,29 @@ describe("BudgetPage — the grid's target and the card (#400)", () => {
|
||||||
it("hands the grid the same callback at every render, so the grid does not report at every render", () => {
|
it("hands the grid the same callback at every render, so the grid does not report at every render", () => {
|
||||||
expect(render().report).toBe(render().report);
|
expect(render().report).toBe(render().report);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("a profile without transactions: the grid's last report does not outlive the grid", () => {
|
||||||
|
// Steps 3 to 5 skipped: the budget step is next on a profile without any
|
||||||
|
// transaction. The rows can land before the count (both reads queue
|
||||||
|
// behind db.ts's lock): the grid mounts, reports "Expand all", then gives
|
||||||
|
// way to the empty state (review of #400).
|
||||||
|
const counters = { ...BUDGET_LEFT, imports: 0, manualCategorized: 0, transactions: 0 };
|
||||||
|
const checklist = computeChecklist(counters, {
|
||||||
|
importFolderSet: true,
|
||||||
|
budgetAllowed: true,
|
||||||
|
skipped: ["source", "import", "manualCategory"],
|
||||||
|
});
|
||||||
|
harness.onboarding = path(counters, { checklist, nextStep: nextStep(checklist) });
|
||||||
|
const budget = harness.budget as { state: BudgetState };
|
||||||
|
budget.state = { ...budget.state, totalInDb: null };
|
||||||
|
render().report("expandAll");
|
||||||
|
expect(render().model.text?.key).toBe("onboarding.card.budget.collapsed");
|
||||||
|
|
||||||
|
budget.state = { ...budget.state, totalInDb: 0 };
|
||||||
|
harness.cursor = 0;
|
||||||
|
const page = BudgetPage() as AnyElement;
|
||||||
|
expect(find(page, (el) => el.type === BudgetTable)).toHaveLength(0);
|
||||||
|
const [card] = find(page, (el) => el.type === OnboardingStepCard);
|
||||||
|
expect(card.props.model).toEqual({ text: null, done: false, target: null });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue