On the page of the step in progress, a card takes the place of the
banner. `StepCard` is driven by props: an h2 title that takes the focus
("Step n of N", then the step), the page's sentence, a status region
always rendered that receives "Done.", "Skip this step" until the step is
done, then "Next step" or the end of the path. `OnboardingStepCard` is
the only piece that reads the path: it renders on the latched card page
only, so the card survives its own success, focuses its title once per
arrival by "Next step" and consumes the navigation state.
The control to use carries `data-onboarding-highlight`, drawn in
styles.css as a 3px accent outline with a pulse bounded to three cycles
and none under prefers-reduced-motion; a test reads the stylesheet and
checks 3:1 against the background and the cards in both themes. The
target is passed by props to EmptyState's primary action, the first
SourceList card and WizardNavigation's next button, never queried.
`importCardModel` reads the wizard and the checklist in order: importing
before the counters, a report that added nothing as a failure, and the
"steps 3 and 4 checked" row at rest, where it can apply. CARD_PAGES gets
/import; the Import page builds the input, renders the card first and
highlights the model's target only while the card shows, and its empty
state waits for the folder to be read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.3 KiB
TypeScript
63 lines
2.3 KiB
TypeScript
// SourceList and SourceCard — the getting-started target (#398). No jsdom:
|
|
// react-dom/server renders in vitest's node environment, and the markup says
|
|
// which card carries the highlight.
|
|
import { describe, it, expect, vi } from "vitest";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
|
|
vi.mock("react-i18next", () => ({
|
|
useTranslation: () => ({
|
|
t: (k: string, o?: Record<string, unknown>) =>
|
|
o ? `${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : k,
|
|
i18n: { language: "fr" },
|
|
}),
|
|
}));
|
|
|
|
import SourceList from "./SourceList";
|
|
import { SOURCE_CARD_TARGET } from "./SourceCard";
|
|
import type { ScannedSource } from "../../shared/types";
|
|
|
|
const HIGHLIGHT = 'data-onboarding-highlight="true"';
|
|
|
|
const source = (name: string): ScannedSource => ({
|
|
folder_name: name,
|
|
folder_path: `/releves/${name}`,
|
|
files: [{ filename: "releve.csv", file_path: `/releves/${name}/releve.csv`, size_bytes: 10, modified_at: "" }],
|
|
});
|
|
|
|
const SOURCES = [source("Desjardins"), source("Tangerine"), source("RBC")];
|
|
|
|
const markup = (onboardingTarget?: boolean, sources: ScannedSource[] = SOURCES) =>
|
|
renderToStaticMarkup(
|
|
<SourceList
|
|
sources={sources}
|
|
configuredSourceNames={new Set()}
|
|
importedFileNames={new Map()}
|
|
onSelectSource={vi.fn()}
|
|
onboardingTarget={onboardingTarget}
|
|
/>,
|
|
);
|
|
|
|
/** The source cards, in order: each is a button. */
|
|
const cards = (html: string) => html.split("<button").slice(1);
|
|
|
|
describe("SourceList — getting-started target (#398)", () => {
|
|
it("highlights the first card of the list, and only it", () => {
|
|
const html = markup(true);
|
|
expect(html.split(HIGHLIGHT).length - 1).toBe(1);
|
|
const [first, ...others] = cards(html);
|
|
expect(first).toContain(HIGHLIGHT);
|
|
expect(first).toContain(`data-onboarding-target="${SOURCE_CARD_TARGET}"`);
|
|
expect(first).toContain("Desjardins");
|
|
expect(others).toHaveLength(2);
|
|
for (const card of others) expect(card).not.toContain("data-onboarding");
|
|
});
|
|
|
|
it("highlights the only card of a one-source list", () => {
|
|
expect(markup(true, [source("Desjardins")]).split(HIGHLIGHT).length - 1).toBe(1);
|
|
});
|
|
|
|
it("highlights nothing without the property", () => {
|
|
expect(markup()).not.toContain("data-onboarding");
|
|
expect(markup(false)).not.toContain("data-onboarding");
|
|
});
|
|
});
|