docs(onboarding): user guide, architecture and QA checklist for the guided getting-started path #407
2 changed files with 50 additions and 3 deletions
|
|
@ -16,6 +16,7 @@ import {
|
|||
initTransactionsState,
|
||||
showsEmptyState,
|
||||
GUIDED_ENTRY_FILTERS,
|
||||
type TransactionsAction,
|
||||
type TransactionsState,
|
||||
} from "./useTransactions";
|
||||
import type { TransactionPageResult } from "../shared/types";
|
||||
|
|
@ -170,6 +171,32 @@ describe("transactionsReducer — the guided entry's fallback (#399)", () => {
|
|||
expect(after.guidedFallback).toBe(false);
|
||||
});
|
||||
|
||||
// The actions the hook really sends around the first page: `fetchData` sends
|
||||
// SET_LOADING then SET_ERROR(null) before each read, and the mount effect's
|
||||
// categories, sources and count land before or after that page. None of
|
||||
// them may disarm the fallback (review of #399).
|
||||
it.each([
|
||||
["the mount reads land first", ["mount", "fetch", "page"]],
|
||||
["the first page lands first", ["fetch", "page", "mount"]],
|
||||
] as const)("the hook's real sequence still falls back (%s)", (_name, order) => {
|
||||
const sent: Record<(typeof order)[number], TransactionsAction[]> = {
|
||||
fetch: [
|
||||
{ type: "SET_LOADING", payload: true },
|
||||
{ type: "SET_ERROR", payload: null },
|
||||
],
|
||||
mount: [
|
||||
{ type: "SET_CATEGORIES", payload: [] },
|
||||
{ type: "SET_SOURCES", payload: [] },
|
||||
{ type: "SET_TOTAL_IN_DB", payload: 12 },
|
||||
],
|
||||
page: [{ type: "SET_PAGE_RESULT", payload: page(0) }],
|
||||
};
|
||||
const after = order.flatMap((group) => sent[group]).reduce(transactionsReducer, guided());
|
||||
expect(after.filters.uncategorizedOnly).toBe(false);
|
||||
expect(after.filters.dateFrom).toBeNull();
|
||||
expect(after.isLoading).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back once: an empty page after the fallback keeps the filters, so nothing reads again", () => {
|
||||
const fellBack = result(guided(), 0);
|
||||
const again = result(fellBack, 0);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import type { TransactionRow } from "../shared/types";
|
||||
import type { TransactionFilters, TransactionRow } from "../shared/types";
|
||||
|
||||
// The page's services pull the database: stub the connection.
|
||||
vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() }));
|
||||
|
|
@ -21,7 +21,12 @@ vi.mock("react-i18next", () => ({
|
|||
}));
|
||||
vi.mock("../components/shared/PageHelp", () => ({ PageHelp: () => null }));
|
||||
|
||||
const harness = vi.hoisted(() => ({ onboarding: null as unknown, rows: [] as TransactionRow[] }));
|
||||
const harness = vi.hoisted(() => ({
|
||||
onboarding: null as unknown,
|
||||
rows: [] as TransactionRow[],
|
||||
/** Filters a later dispatch would have set (the fallback, a filter picked by hand). */
|
||||
filters: null as Partial<TransactionFilters> | null,
|
||||
}));
|
||||
|
||||
vi.mock("../contexts/OnboardingContext", () => ({ useOnboarding: () => harness.onboarding }));
|
||||
vi.mock("../hooks/useTransactions", async (importOriginal) => {
|
||||
|
|
@ -30,7 +35,8 @@ vi.mock("../hooks/useTransactions", async (importOriginal) => {
|
|||
...actual,
|
||||
useTransactions: (...args: Parameters<typeof actual.useTransactions>) => {
|
||||
const hook = actual.useTransactions(...args);
|
||||
return { ...hook, state: { ...hook.state, rows: harness.rows } };
|
||||
const filters = harness.filters ? { ...hook.state.filters, ...harness.filters } : hook.state.filters;
|
||||
return { ...hook, state: { ...hook.state, rows: harness.rows, filters } };
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -122,6 +128,7 @@ const activePeriod = (html: string) =>
|
|||
beforeEach(() => {
|
||||
harness.onboarding = path(IMPORTED);
|
||||
harness.rows = [];
|
||||
harness.filters = null;
|
||||
});
|
||||
|
||||
describe("TransactionsPage — the guided entry (#399)", () => {
|
||||
|
|
@ -165,6 +172,19 @@ describe("TransactionsPage — the step card (#399)", () => {
|
|||
expect(sentence(render())).toBe("t(onboarding.card.transactions.all[])");
|
||||
});
|
||||
|
||||
// The sentence follows the list's filter, not the navigation state.
|
||||
it("the guided entry after its fallback: the sentence for every transaction", () => {
|
||||
harness.filters = { uncategorizedOnly: false };
|
||||
expect(sentence(render(GUIDED))).toBe("t(onboarding.card.transactions.all[])");
|
||||
});
|
||||
|
||||
it("an ordinary visit with 'Uncategorized' picked by hand: the uncategorized sentence", () => {
|
||||
harness.filters = { uncategorizedOnly: true };
|
||||
expect(sentence(render())).toBe(
|
||||
"t(onboarding.card.transactions.uncategorized[filter=t(transactions.filters.uncategorized)])",
|
||||
);
|
||||
});
|
||||
|
||||
it("the first row's category is the one target", () => {
|
||||
harness.rows = [row(1, "IGA 123"), row(2, "METRO 456")];
|
||||
const html = render(GUIDED);
|
||||
|
|
|
|||
Loading…
Reference in a new issue