Test application behavior with Vitest
Outcome
Section titled “Outcome”You will add a repeatable automated test path to Delivery Board. Vitest will run TypeScript and TSX tests through the Vite module graph. React Testing Library will render components into a jsdom document, and user-event will operate controls through the same roles and accessible names that a user can find.
The final suite will check an empty collection, a filter restored from the URL, a work-item status transition, focus recovery after a filtered item disappears, an unknown work-item route, and route title and focus behavior.
Why this matters
Section titled “Why this matters”TypeScript can reject an invalid relationship between types. A linter can reject a configured source pattern. A production build can prove that the current source creates output. None of those checks proves that activating Start work changes the intended record, reports the result, or leaves keyboard focus in a usable place.
An automated behavior test records selected application contracts as executable evidence. It can repeat those contracts after a refactor or dependency change. The test suite does not replace browser inspection, accessibility evaluation, or product judgment. It gives those checks a smaller and more reliable regression surface.
What you will practice
- Distinguish type checking, linting, production building, automated behavior tests, and real-browser verification by the evidence each can provide.
- Choose a test seam from a user-visible outcome and a plausible regression instead of testing every source line.
- Configure Vitest through the existing Vite configuration with a jsdom environment and one setup file.
- Use React Testing Library queries based on roles, accessible names, labels, text, and current values.
- Choose getBy, queryBy, or findBy from the expected timing and presence of an element.
- Use userEvent.setup and await interactions that can cause React updates.
- Render the routed client with MemoryRouter and an explicit initial URL.
- Check rendered state, absence, live feedback, focus, document title, and current navigation without reading React internals.
- Read one focused failure from the assertion, accessible DOM output, and source location before changing code.
- Explain why jsdom component tests do not prove responsive layout, CSS rendering, browser history integration, or complete accessibility.
What is new and what is reused
Section titled “What is new and what is reused”- New: Test case, test oracle, regression risk, test seam, Vitest, jsdom, React Testing Library, jest-dom matchers, user-event,
describe,it,expect,vi.fn,render,screen,within,MemoryRouter,getBy,queryBy,findBy, test isolation, cleanup, focused test runs, watch mode, and a controlled sensitivity check. - Reused: The clean
feature/react-componentsbranch, Vite, TypeScript, React, React Router declarative mode, typed fixtures, status transitions, URL search state, empty results, live status feedback, heading focus, document titles, semantic HTML, accessible names, source checks, production preview, browser testing, and focused Git commits.
Starting point
Before you start
- The completed Build routes and URL state with React Router lesson on a clean feature/react-components branch.
- Delivery Board defines the board, status guide, work-item detail, and catch-all routes.
- The board can filter through URL search state and can update the current in-memory work-item collection.
- A filtered status update moves focus to live feedback when the active card leaves the rendered result.
- The project passes typecheck, lint, build, and production-preview browser checks before test packages are installed.
- No API request, server process, database, authentication rule, coverage percentage, end-to-end browser runner, or visual-regression service is required in this lesson.
- Current state
- The routed client has a documented manual test path, but no command can repeat selected component, state, route, URL, and focus contracts after a source change.
- First action
- Open the delivery-board repository, verify the clean routing commit on feature/react-components, run the baseline source checks, and write six named test outcomes before installing a test package.
- First checkpoint
- Vitest runs one passing WorkItemList test in jsdom, the test finds the empty result through visible semantics, and no application source changed to make the test pass.
- Help trigger
- Use the nearest recovery note or ask for help if no tests are discovered, document is undefined, a jest-dom matcher has no type, a router Hook lacks context, a query finds several elements, an awaited interaction still reports an act warning, the focused test fails for a different reason than its name describes, or test and browser results disagree.
Required result
Section titled “Required result”You have completed the lesson when:
- the work continues from the clean routing commit on
feature/react-components; - the test plan names six user-visible contracts and one plausible wrong implementation for each contract;
vitest,jsdom,@testing-library/react,@testing-library/jest-dom, and@testing-library/user-eventare recorded as development dependencies;package.jsonprovidestestfor one complete deterministic run andtest:watchfor an optional local watch session;- the existing
vite.config.tskeeps the React plugin and adds ajsdomtest environment plus one setup file; src/test/setup.tsadds Vitest-compatible jest-dom matchers and resets the rendered document after every test;WorkItemList.test.tsxchecks the empty result and confirms that an empty list is not rendered;App.test.tsxrenders the realAppinsideMemoryRouterwith an explicit initial entry;- one test proves that
/?status=plannedselects Planned, renders the planned record, excludes an active record, and reports the correct counts; - one test activates Start work, then checks the changed visible status, next accessible action, retained button focus, and live result message;
- one test proves that changing the only planned item removes its card, renders the empty result, and moves focus to the status message;
- one test proves that
/work-items/WI-999renders the specific missing-record result and recovery link; - one test navigates to the status guide and checks the page heading, heading focus, document title, and current navigation state;
- tests use roles, accessible names, labels, visible text, and current values before any lower-level DOM selector;
- the suite does not inspect Hook state, component instances, CSS class names, private functions, or implementation call order;
- the controlled wrong status transition makes the focused behavior test fail for the intended reason, and the correct transition is restored before the final run;
- six tests pass in two files through
npm test; - typecheck, lint, build, and the routed production-preview browser checks still pass;
README.mddocuments the two test commands, six automated scenarios, test-file responsibilities, jsdom limits, required browser checks, and latest verified result; and- the final diff contains test setup, test source, dependency records, scripts, and matching README guidance in one focused commit.
Resume the verified routing branch
Section titled “Resume the verified routing branch”Run these commands from the folder that contains Delivery Board’s package.json:
git switch feature/react-componentsgit statusgit log -1 --onelinenpm cinpm run typechecknpm run lintnpm run buildConfirm that the latest commit is the routing lesson result and that git status is clean. Open the production preview once and repeat the route checks if the branch has changed since the routing lesson.
Stop before installing packages if a baseline check fails. A new test runner cannot repair an existing source, dependency, or production-build failure.
Decide what each check must prove
Section titled “Decide what each check must prove”A useful test starts from a contract, not from a source file count. State the outcome, the plausible regression, and the smallest boundary that can expose it.
Use this plan for the required suite:
| Contract | Plausible regression | Smallest useful test seam |
|---|---|---|
| An empty collection gives a specific result and no empty list | The component renders an empty list or hides the supplied message | WorkItemList with an empty array |
| A shared filtered URL reproduces the planned view | The select and cards ignore or disagree with the search parameter | App with MemoryRouter at /?status=planned |
| Start work changes the selected record and reports Active | The status cycle skips a value or updates the wrong item | App plus one accessible button interaction |
| A removed filtered card does not lose keyboard position | The focused DOM node disappears and focus returns to the document body | App at the planned URL plus one interaction |
| An unknown item ID gives a recovery result | The detail route crashes or renders unrelated data | App at /work-items/WI-999 |
| Client navigation updates page context | The new view renders but title, heading focus, or current navigation stays stale | App plus the Status guide link |
Do not add one test for every prop, line, component, route, or CSS selector. These six tests cover boundaries with meaningful regression risk. More tests are useful only when another contract or failure justifies them.
Match the tool to the evidence
Section titled “Match the tool to the evidence”| Check | Evidence it can provide | Evidence it cannot provide alone |
|---|---|---|
npm run typecheck |
Type relationships satisfy the configured TypeScript project | A button produces the intended runtime result |
npm run lint |
Source satisfies configured static rules | The interface is usable or correct |
npm test |
Selected behavior matches executable expectations in the configured test environment | Complete browser, layout, network, or accessibility behavior |
npm run build |
Current source produces production assets | Every route and interaction works after load |
| Production-preview browser check | The built client works in the named browser and viewport | Future changes keep working without repeating the check |
All five checks answer different questions. A green test suite does not cancel a failed production build or a clipped 320-pixel layout.
A short PHP comparison
Section titled “A short PHP comparison”PHP projects can use a runner such as PHPUnit to execute server-side units and integrations. This client uses Vitest because Vitest shares Vite’s TypeScript, TSX, module-resolution, and plugin configuration.
The transferable model is the same:
- Arrange a known input and starting state.
- Act through a public boundary.
- Assert the observable result.
- Reset shared state before the next independent case.
The runtime boundary differs. PHPUnit normally executes PHP on the server. This lesson executes client TypeScript and React behavior through Node and a simulated DOM. Later Express lessons will apply the same test-case model to HTTP requests and JSON responses.
Install the bounded test stack
Section titled “Install the bounded test stack”From the project root, install the five development tools:
npm install --save-dev vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-eventThen inspect the resolved versions:
npm ls vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-eventThe command records direct ranges in package.json and exact resolved packages in package-lock.json. Keep the lockfile. Do not replace the installed versions with numbers copied from this lesson or another project.
Each package has one responsibility:
| Package | Responsibility in this lesson |
|---|---|
vitest |
Discover test files, run cases, provide assertions and test doubles, and report failures |
jsdom |
Supply a DOM-like document and browser APIs inside Node |
@testing-library/react |
Render React elements and query the resulting document |
@testing-library/jest-dom |
Add readable DOM assertions such as toBeInTheDocument and toHaveFocus |
@testing-library/user-event |
Simulate user interaction sequences such as clicking and keyboard input |
These are development dependencies because the production browser bundle does not run the test suite.
Add deterministic and watch scripts
Section titled “Add deterministic and watch scripts”Open package.json. Add these entries inside the existing scripts object:
"test": "vitest run","test:watch": "vitest"Keep the existing dev, typecheck, build, lint, and preview scripts.
npm testruns the complete suite once and exits with success or failure. Use this command in the final quality gate.npm run test:watchstays open, observes source changes, and reruns related tests. Use it during a focused edit when continuous feedback helps.
The explicit run mode prevents a required check from waiting indefinitely for another file change.
Configure Vitest through Vite
Section titled “Configure Vitest through Vite”Replace vite.config.ts with this configuration:
import react from "@vitejs/plugin-react";import { defineConfig } from "vitest/config";
export default defineConfig({ plugins: [react()], test: { environment: "jsdom", setupFiles: ["./src/test/setup.ts"], },});defineConfig from vitest/config accepts the existing Vite options and the test property. The React plugin remains active, so test files use the same TSX transformation as application modules.
The jsdom environment provides document, HTMLElement, accessible DOM properties, focus state, and other browser-like APIs while the tests run in Node. It does not render pixels or run a complete browser engine.
Add one explicit setup file
Section titled “Add one explicit setup file”Create src/test/setup.ts:
import "@testing-library/jest-dom/vitest";import { cleanup } from "@testing-library/react";import { afterEach } from "vitest";
afterEach(() => { cleanup(); document.title = "";});The setup file runs before every test file:
- the Vitest-specific jest-dom import extends
expectwith DOM matchers; cleanup()removes the rendered React tree after every test; and- resetting
document.titleprevents one route test from supplying the next test’s starting title.
Vitest globals remain disabled. Each test file will import describe, it, expect, and vi explicitly. The explicit afterEach import also makes cleanup independent of a global test API.
The project now has this test structure:
Directorysrc
Directorycomponents
- WorkItemCard.tsx
- WorkItemList.tsx
- WorkItemList.test.tsx — Focused empty-result component test
Directorytest
- setup.ts — DOM matchers and per-test reset
- App.test.tsx — Routed client behavior tests
- App.tsx
- package.json — Test scripts and development dependencies
- package-lock.json — Exact dependency graph
- vite.config.ts — Shared Vite and Vitest configuration
Use queries that describe the interface
Section titled “Use queries that describe the interface”Testing Library queries describe how an element can be found in the rendered interface.
Prefer this order when the interface supports it:
getByRolewith an accessible name for buttons, links, headings, navigation, lists, and status regions.getByLabelTextorgetByRolewith a name for form controls.- Visible text or current display value for non-interactive content.
- A test ID only when no semantic or visible contract can identify the element.
Do not select .work-item-action, inspect a React state variable, or call a component handler directly when the contract concerns a user activating a named button.
Choose the query family from the expected result
Section titled “Choose the query family from the expected result”| Query family | Use when | Failure behavior |
|---|---|---|
getBy... |
One matching element must exist now | Throws for zero or several matches |
queryBy... |
One matching element must be absent | Returns null for zero; throws for several |
findBy... |
One matching element should appear after asynchronous work | Retries until it finds the element or times out |
Use an All variant when several matches are the expected contract. Do not change to getAllBy... only to silence an unexpected duplicate. First decide whether several elements are correct.
Write the first focused component test
Section titled “Write the first focused component test”Create src/components/WorkItemList.test.tsx:
import { render, screen } from "@testing-library/react";import { describe, expect, it, vi } from "vitest";import { WorkItemList } from "./WorkItemList";
describe("WorkItemList", () => { it("shows the supplied empty result without an empty list", () => { render( <WorkItemList items={[]} emptyMessage="No planned work items match this filter." onStatusChange={vi.fn()} />, );
expect( screen.getByRole("heading", { name: "Current work" }), ).toBeInTheDocument(); expect( screen.getByText("No planned work items match this filter."), ).toBeInTheDocument(); expect(screen.queryByRole("list")).not.toBeInTheDocument(); });});The test has three visible phases:
- Arrange: render
WorkItemListwith no items, a specific message, and a valid callback prop. - Act: no interaction is required because the empty result exists on the initial render.
- Assert: check the section heading, supplied message, and absence of a list.
vi.fn() supplies the required callback boundary. This test does not claim that the callback ran. Its evidence concerns only the empty render result.
Run the file alone:
npm test -- src/components/WorkItemList.test.tsxThe expected summary reports one passed file and one passed test.
If it does not work
Section titled “If it does not work”- No test file found: confirm the filename contains
.test.tsxand remains under the project root. document is not defined: confirmenvironment: "jsdom"is inside thetestproperty that Vitest reads.toBeInTheDocumenthas no type: confirm the setup file is TypeScript, its path is correct, and it imports@testing-library/jest-dom/vitest.- The list query finds a list: inspect the accessible DOM in the failure output. Confirm that
items={[]}reaches the intended component and that the application component has not been rendered around it.
Checkpoint: One isolated render contract is executable
- What now works
- Vitest discovers the TSX file, jsdom supplies a document, WorkItemList renders the supplied empty result, and the test confirms that no empty list exists.
- Files changed
package.json, package-lock.json, vite.config.ts, src/test/setup.ts, src/components/WorkItemList.test.tsx- What remains
- Render the complete routed client and test interactions, URL state, recovery, title, and focus through public behavior.
- Next action
- Create src/App.test.tsx and add the shared MemoryRouter render helper plus the five application tests.
- If it does not work
- Run only WorkItemList.test.tsx, read the first configuration or query error, and repair that boundary before adding the routed test environment.
Render the routed client in memory
Section titled “Render the routed client in memory”The production entry uses BrowserRouter, but App receives router context from its parent. A test can provide MemoryRouter instead:
function renderApp(initialEntry = "/") { return render( <MemoryRouter initialEntries={[initialEntry]}> <App /> </MemoryRouter>, );}MemoryRouter stores location entries in memory. initialEntries gives each test a specific route and search string without changing the real browser history. The test still renders the real App, route table, pages, state owner, and router-aware links.
Do not wrap App in BrowserRouter inside the test. Tests should not compete over one global browser URL when an isolated in-memory route can express the same component contract.
Add the routed application tests
Section titled “Add the routed application tests”Create src/App.test.tsx:
import { render, screen, waitFor, within } from "@testing-library/react";import userEvent from "@testing-library/user-event";import { MemoryRouter } from "react-router";import { describe, expect, it } from "vitest";import App from "./App";
function renderApp(initialEntry = "/") { return render( <MemoryRouter initialEntries={[initialEntry]}> <App /> </MemoryRouter>, );}
describe("Delivery Board", () => { it("reproduces a valid status filter from the URL", () => { renderApp("/?status=planned");
expect( screen.getByRole("combobox", { name: "Show work items" }), ).toHaveValue("planned"); expect( screen.getByRole("heading", { name: "Prepare the accessible navigation review", }), ).toBeInTheDocument(); expect( screen.queryByRole("heading", { name: "Document the component responsibilities", }), ).not.toBeInTheDocument(); expect( screen.getByText("1 work item shown. 1 of 3 done."), ).toBeInTheDocument(); });
it("updates the selected work item and reports the result", async () => { const user = userEvent.setup(); renderApp();
const itemHeading = screen.getByRole("heading", { name: "Prepare the accessible navigation review", }); const item = itemHeading.closest("article");
if (!(item instanceof HTMLElement)) { throw new Error("Expected the work-item heading inside an article."); }
await user.click( within(item).getByRole("button", { name: "Start work: Prepare the accessible navigation review", }), );
expect( within(item).getByText("Active"), ).toBeInTheDocument(); expect( within(item).getByRole("button", { name: "Mark done: Prepare the accessible navigation review", }), ).toHaveFocus(); expect(screen.getByRole("status")).toHaveTextContent( "Prepare the accessible navigation review changed to Active.", ); });
it("moves focus to feedback when a filtered item leaves the result", async () => { const user = userEvent.setup(); renderApp("/?status=planned");
await user.click( screen.getByRole("button", { name: "Start work: Prepare the accessible navigation review", }), );
const status = screen.getByRole("status");
expect(status).toHaveTextContent( "Prepare the accessible navigation review changed to Active.", ); expect(status).toHaveFocus(); expect( screen.queryByRole("heading", { name: "Prepare the accessible navigation review", }), ).not.toBeInTheDocument(); expect( screen.getByText("No planned work items match this filter."), ).toBeInTheDocument(); });
it("shows a recovery page for an unknown work-item ID", () => { renderApp("/work-items/WI-999");
expect( screen.getByRole("heading", { name: "Work item not found" }), ).toBeInTheDocument(); expect(screen.getByText("WI-999")).toBeInTheDocument(); expect( screen.getByRole("link", { name: "Return to the board" }), ).toBeInTheDocument(); });
it("updates the title and heading focus after client navigation", async () => { const user = userEvent.setup(); renderApp();
await user.click( screen.getByRole("link", { name: "Status guide" }), );
const heading = screen.getByRole("heading", { name: "Status guide" });
await waitFor(() => { expect(heading).toHaveFocus(); }); expect(document.title).toBe("Status guide | Delivery Board"); expect( screen.getByRole("link", { name: "Status guide" }), ).toHaveAttribute("aria-current", "page"); });});Run the application file:
npm test -- src/App.test.tsxThe expected summary reports one passed file and five passed tests.
Keep the selected card boundary explicit
Section titled “Keep the selected card boundary explicit”The page contains three buttons and three links with related visible text. The status-transition test first finds the intended work-item heading, then moves to its containing article, and then uses within(item).
This scope expresses the contract: activate the named button inside the card for Prepare the accessible navigation review. It does not depend on array position or a CSS class.
The instanceof HTMLElement guard validates the result of closest at runtime and narrows its TypeScript type. Do not replace the guard with a non-null assertion. A changed semantic structure should produce a clear test failure rather than an invalid assumption.
Await interaction sequences
Section titled “Await interaction sequences”Create a user-event session inside each interactive test:
const user = userEvent.setup();
await user.click( screen.getByRole("link", { name: "Status guide" }),);user-event dispatches the related pointer, mouse, focus, and click events for the requested action. Its methods can trigger asynchronous React work, so await them.
Do not place one shared user session in beforeEach. Each test must make its starting input-device state visible.
Test focus as behavior
Section titled “Test focus as behavior”Two tests check different focus contracts:
- When a card remains in the DOM after its status changes, the same button keeps focus and receives a new accessible name.
- When a filtered card leaves the DOM, the live status message receives focus so keyboard position does not fall back to an unexplained document location.
The route test uses waitFor because the focus change happens in an effect after client navigation. waitFor retries the assertion until it passes or reaches its timeout. It does not add an arbitrary delay.
Do not test state variables
Section titled “Do not test state variables”The suite never asks for the current items state, selected filter Hook, or effect call count. It checks the results those mechanisms exist to produce:
- the selected value;
- visible and absent records;
- updated status and action;
- live feedback;
- focus;
- route recovery;
- document title; and
aria-currenton current navigation.
A future refactor can change component boundaries or Hook arrangement while preserving these contracts.
Checkpoint: The routed client has selected regression coverage
- What now works
- Five App tests reproduce URL state, operate the real status flow, verify both focus outcomes, recover from an unknown item ID, and check route title and current-page behavior through MemoryRouter.
- Files changed
src/App.test.tsx, src/components/WorkItemList.test.tsx, src/test/setup.ts, vite.config.ts- What remains
- Confirm that the tests detect a plausible defect, restore the source, run the complete quality gate, and repeat the checks that require a real browser.
- Next action
- Run npm test, then perform the controlled wrong-transition sensitivity check against the named status test.
- If it does not work
- Run one failing test by file and exact name, read its assertion and accessible DOM output, and repair the earliest setup, query, action, or expectation boundary.
Confirm that a test can detect the wrong behavior
Section titled “Confirm that a test can detect the wrong behavior”A passing test can still be too weak. Perform one controlled mutation to prove that the status test rejects a plausible wrong implementation.
In src/components/WorkItemCard.tsx, temporarily change only this mapping:
const nextStatusByStatus: Record<WorkStatus, WorkStatus> = { planned: "done", active: "done", done: "planned",};Run only the matching test:
npm test -- src/App.test.tsx -t "updates the selected work item"The focused run must fail because the card shows Done rather than Active. The failure should point to the expected visible status or next action. A configuration error, missing document, or router-context error would not prove this test’s sensitivity.
Restore the correct mapping immediately:
const nextStatusByStatus: Record<WorkStatus, WorkStatus> = { planned: "active", active: "done", done: "planned",};Run the complete suite again:
npm testThe final result must report two passed files and six passed tests. Confirm that git diff does not contain the temporary wrong transition.
Use watch mode for a focused edit
Section titled “Use watch mode for a focused edit”Start watch mode when repeated local feedback helps:
npm run test:watchVitest reports the active file and key commands. Edit one test description without changing behavior, save, and observe the related rerun. Press q to exit after the experiment.
Do not use watch mode as the final evidence. Run npm test once after watch mode exits so the complete suite starts from one clean process and returns a final exit code.
Diagnose the earliest failing boundary
Section titled “Diagnose the earliest failing boundary”Read the test name, assertion message, accessible DOM output, and first source location before editing.
| Symptom | Likely boundary | Focused check |
|---|---|---|
| No test files found | Discovery | Confirm .test.tsx filename and project path |
document is not defined |
Environment | Confirm test.environment is jsdom in the loaded Vite config |
| DOM matcher is missing | Setup | Confirm the setup path and /vitest jest-dom import |
| Router Hook lacks context | Provider | Render App inside one MemoryRouter |
| Query finds several elements | Test scope or interface name | Use the intended region or card with within; do not select the first match by index without a contract |
| Query finds no named control | Accessible interface or stale expectation | Inspect the rendered roles and names before changing the query |
act warning after interaction |
Async boundary | Create user-event inside the test and await the action or resulting async query |
| Test passes alone but fails in the suite | Isolation | Check cleanup, mutable module data, mocks, title, timers, and shared globals |
| Focus assertion fails | DOM lifetime or effect timing | Confirm the intended element remains or use waitFor for effect-driven navigation focus |
| Test says Active is missing and DOM shows Done | Application transition | Compare the status map with the expected planned → active contract |
| Test is green but the browser is wrong | Environment limit or missing case | State which browser behavior is outside jsdom and add the smallest suitable browser or end-to-end check |
Repair the earliest boundary that explains the evidence. Do not weaken an expectation only because the current implementation disagrees with the product contract.
Treat an accessible query failure as interface evidence
Section titled “Treat an accessible query failure as interface evidence”If getByRole("button", { name: ... }) cannot find a control that users must identify, inspect the rendered roles and names. The source may have an inaccessible name, the test may contain stale product wording, or the control may not exist in the current state.
Choose the correction from that evidence. A CSS selector can make the test green while preserving a missing accessible name.
Keep jsdom inside its evidence boundary
Section titled “Keep jsdom inside its evidence boundary”jsdom models many document APIs in Node. It does not provide a complete visual or browser environment.
The required tests can check:
- semantic roles and accessible names derived from the DOM;
- control values and visible text;
- event-driven React updates;
- presence and absence of elements;
- focus state assigned through DOM APIs;
- document title; and
- declarative routing with an in-memory history.
The required tests cannot prove:
- CSS layout at 320 pixels or 200% zoom;
- text clipping, horizontal overflow, paint order, or color contrast;
- the complete browser pointer, keyboard, focus, and History API implementation;
- direct server handling of
/work-items/WI-001; - screen-reader announcements or usability with assistive technology;
- production network, performance, security, or device behavior; or
- that every important product outcome has a test.
Passing getByRole queries is useful accessibility evidence because the DOM exposes usable semantics. It is not a complete accessibility audit.
Run the complete quality gate
Section titled “Run the complete quality gate”From the project root, run each required command:
npm testnpm run typechecknpm run lintnpm run buildExpected evidence:
- two test files and six tests pass;
- TypeScript reports no error;
- the linter reports no unresolved error; and
- Vite creates the production
distoutput.
If a later command fails, the change is not ready because the earlier command passed. Read the first failing command’s evidence and repair that boundary.
Repeat the real-browser regression path
Section titled “Repeat the real-browser regression path”Start the production preview:
npm run previewUse the exact URL Vite reports. Repeat these checks in a current browser:
- Open
/,/?status=planned,/guide,/work-items/WI-001,/work-items/WI-999, and/does-not-existdirectly. - Reload each path and confirm the correct route result.
- Change the planned item at
/and confirm its visible status, live message, and retained action-button focus. - Change the only item at
/?status=plannedand confirm the empty result plus status-message focus. - Use Board, Status guide, Back, and Forward. Confirm current navigation, titles, headings, and focus.
- Check keyboard operation, visible focus, 320 CSS pixels, 200% zoom, long content, and horizontal overflow.
- Confirm that the Console contains no unresolved React, router, or application error.
Stop the preview with Ctrl+C after the checks.
Record these browser results beside the automated result in the project documentation. Keep the environment and date visible. Do not report jsdom tests as browser evidence.
Update the test handoff in README
Section titled “Update the test handoff in README”Update the existing README.md while the evidence is current. Keep these details in its test procedure or quality section:
- Commands:
npm testruns the complete suite once;npm run test:watchstarts an optional local watch session. - Test files:
src/components/WorkItemList.test.tsxchecks the isolated empty result;src/App.test.tsxchecks routed application behavior;src/test/setup.tsowns shared DOM setup and reset. - Automated scenarios: list the empty collection, planned URL, status transition, filtered focus recovery, unknown item ID, and route title-and-focus cases.
- Environment: state that these tests run in jsdom through Node and do not run a full browser engine.
- Required browser evidence: retain the direct-route, keyboard, focus, narrow-width, 200%-zoom, overflow, and Console checks.
- Latest verified result: record the test count, automated command results, named browser, browser version, operating system, viewport checks, test date, and any evidence-backed limit.
Do not paste terminal logs into README. Record the result and conditions another contributor needs to repeat the checks. Do not include a username, local absolute path, token, or private student data.
Inspect and commit the test change
Section titled “Inspect and commit the test change”Inspect the complete project diff:
git status --shortgit diffThe expected change contains:
package.jsonandpackage-lock.jsonfor test tools and scripts;vite.config.tsfor the test environment and setup path;- new
src/test/setup.ts; - new
src/components/WorkItemList.test.tsx; and - new
src/App.test.tsx; and - updated
README.mdtest commands, coverage boundary, browser procedure, and latest evidence.
No production application source must remain changed. The temporary wrong status transition must not appear in the diff. Generated dist, coverage output, and node_modules remain outside Git.
Stage the exact files:
git add package.json package-lock.json vite.config.tsgit add src/test/setup.ts src/App.test.tsxgit add src/components/WorkItemList.test.tsxgit add README.mdgit diff --cachedgit commit -m "test: add React behavior coverage"git statusThe final status must be clean on feature/react-components. Keep all four React lesson commits available for Project stage 2 review.
Self-check
Complete these checks against the required result.
- Confirm that testing started from the clean routing commit and that the baseline source and production checks passed.
- Point to the six planned contracts and name one plausible regression that each test can detect.
- Confirm that all five test tools are development dependencies and that package-lock.json records the resolved graph.
- Explain why test runs once and exits while test:watch remains open for local feedback.
- Confirm that vite.config.ts keeps the React plugin and adds only the jsdom environment and setup path needed now.
- Explain why the setup imports the Vitest-specific jest-dom entry and resets both the React tree and document title.
- Run WorkItemList.test.tsx alone and confirm its heading, visible message, and absent-list assertions.
- Explain why vi.fn supplies a prop in the empty test but does not prove a user-visible callback result.
- Point to one role-and-name query, one visible-text query, one value assertion, and one absence query.
- Explain when getBy, queryBy, and findBy are appropriate.
- Confirm that renderApp supplies an explicit MemoryRouter entry without changing the real browser history.
- Open the planned URL test and trace its arranged route, immediate assertions, and plausible failure.
- Run the status interaction and confirm Active, the next named action, retained focus, and live feedback.
- Run the filtered status interaction and confirm that the card leaves, the empty result appears, and feedback receives focus.
- Run the unknown-item test and confirm the requested ID plus recovery link.
- Navigate to Status guide and confirm heading focus, document title, and aria-current.
- Confirm that every user-event interaction is awaited and that each user session starts inside its test.
- Confirm that the suite does not inspect React state, component instances, CSS classes, or private call order.
- Perform the controlled wrong transition, confirm the focused test fails for visible Done instead of Active, then restore the correct source.
- Run the complete suite and confirm two files and six tests pass in one clean process.
- Run typecheck, lint, and build after the test suite.
- Repeat the direct-route, interaction, focus, keyboard, narrow, zoom, overflow, and Console checks in the production preview.
- State at least four results that jsdom cannot prove and identify the required real-browser evidence for them.
- Confirm that README names both test commands, all six scenarios, test-file responsibilities, jsdom limits, browser checks, and the latest evidence conditions.
- Inspect the final diff, confirm no deliberate defect or generated output remains, create the test commit, and confirm a clean branch.
Explanation: test the contract at the narrowest realistic boundary
Section titled “Explanation: test the contract at the narrowest realistic boundary”The final suite uses two test seams:
WorkItemListis a focused component boundary for a collection with no items.Appis an integration boundary for state, pages, routes, URL search state, effects, and navigation.
The complete application tests are larger, but each test still names one scenario and one outcome. They use real local components and router behavior. No test mocks the component whose behavior it claims to prove.
Accessible queries connect testability and interface quality. A test that finds Start work: Prepare the accessible navigation review uses the same computed name exposed to assistive technology. If that name disappears, the test reports a meaningful interface change.
The suite remains selective. It does not duplicate every manual route case or prove CSS behavior through DOM strings. A useful test portfolio combines static checks, focused unit or component tests, integration tests, and real-browser checks according to the risk and runtime boundary.
Official references
Section titled “Official references”- Vitest: Getting Started — installation, scripts, Vite integration, and command modes
- Vitest: Writing Tests — test discovery and test-file organization
- Vitest: Test Environment —
node,jsdom, and other environment boundaries - Vitest: Setup Files — code that runs before each test file
- Testing Library: Guiding Principles — tests that resemble software use
- Testing Library: About Queries — query types, priority,
screen, and semantic selection - user-event: Introduction — interaction sessions and awaited user actions
- jest-dom — Vitest setup and DOM-specific matchers
- React Router: MemoryRouter — in-memory declarative routing and initial entries
Optional extensions
Section titled “Optional extensions”The six required tests and real-browser checks complete this lesson. Keep each selected extension outside the required commit or identify it in a separate commit.
Next step or safe stopping point
Section titled “Next step or safe stopping point”The required result is safely paused when both test files pass, the controlled defect has been restored, typecheck, lint, and build pass, the production-preview browser path is recorded, the test commit exists, and git status is clean on feature/react-components.
Continue to Project stage 2: Build the React client. That assignment will ask you to apply the component, state, routing, accessibility, responsive, test-design, browser-verification, documentation, and Git practices to the project client as one reviewable product stage.