Skip to content

testing library block wizard patterns

Paulo Gomes da Cruz Junior edited this page Oct 8, 2026 · 1 revision

title: "Test Patterns — Block Creation Wizard" type: pattern-guide domain: frontend framework: react testing: "vitest + @testing-library/react" component: "BlockWizardShell" tags: [testing-library, react-query, vitest, wizard, cache] created: 2026-10-07

Tests for the District Average block wizard cover dirty-tab navigation, reporting-unit-scoped React Query cache entries, and the current simulated create response.

Dirty-tab guard

Render BlockWizardShell with a panel that reads and changes the shared dirty context. Use userEvent to mark the panel dirty and select another tab. Verify that the confirmation dialog appears and the original tab stays selected until the user chooses an action.

const user = userEvent.setup();
render(
  <BlockWizardShell
    panels={[
      <DirtyStateControls key="details" />,
      <p key="area">Area panel</p>,
    ]}
  />,
);

await user.click(await screen.findByRole("button", { name: "Mark dirty" }));
await user.click(screen.getByRole("tab", { name: "Area calculator" }));

expect(await screen.findByRole("dialog")).toBeTruthy();
await user.click(screen.getByRole("button", { name: "Stay on tab" }));
expect(
  screen
    .getByRole("tab", { name: "Block details" })
    .getAttribute("aria-selected"),
).toBe("true");
expect(screen.getByText("Dirty")).toBeTruthy();

Also cover discard: choose Discard changes, then assert that the destination panel appears and the dirty indicator clears. Try another tab after choosing Stay on tab; the dialog should appear again because the dirty state remains. A clean panel should switch tabs without a dialog.

Use accessible tab, dialog, and button roles. Check the selected tab through its aria-selected value. The shell owns the guard. Child panels mark or clear the shared state through useBlockWizardDirty and must render under BlockWizardShell's provider.

Reporting-unit-scoped React Query cache

Block-list query keys include the reporting-unit ID. Seed two keys, assert each returns its own value, and remove both entries afterward:

const firstKey = queryKeys.block.list(468);
const secondKey = queryKeys.block.list(469);
queryClient.setQueryData(firstKey, { page: { totalElements: 1 } });
queryClient.setQueryData(secondKey, { page: { totalElements: 3 } });

expect(queryClient.getQueryData(firstKey)).toEqual({
  page: { totalElements: 1 },
});
expect(queryClient.getQueryData(secondKey)).toEqual({
  page: { totalElements: 3 },
});

queryClient.removeQueries({ queryKey: firstKey });
queryClient.removeQueries({ queryKey: secondKey });

The application exports a singleton queryClient. Remove entries created by a test so state does not leak. For component tests that mount QueryClientProvider, create a client per test when provider isolation matters; disable retries in test defaults where appropriate.

Current simulated create response

ReportingUnitService.createBlock does not call a backend endpoint yet. It waits 300 ms, then returns a fabricated draft resource with id: Date.now(), version: 0, and timestamps from the current time. Its service test uses fake timers, pins system time, advances the timer, and verifies that the HTTP request mock was not called.

Treat this as a stub, not a backend integration test. When the create endpoint ships, replace the stub assertions with request and response contract tests. The service source marks the doRequest POST as the swap point.

CancelablePromise cancellation

The cancellation test attaches a rejection handler before calling cancel(). It then advances fake timers and awaits the derived promise, which resolves to the captured CancelError object. The test also checks isCancelled:

const promise = service.createBlock(468, { blockType: "DISTRICT_AVERAGE" });
const settledPromise = promise.catch((error: unknown) => error);
promise.cancel();
await vi.advanceTimersByTimeAsync(300);

await expect(settledPromise).resolves.toMatchObject({ name: "CancelError" });
expect(promise.isCancelled).toBe(true);

This records the tested assertion pattern only. The branch commit does not establish a deeper runtime or framework cause for the rejection-handling change.

Related guides

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally