docs: add UI test strategy to refactor spec

This commit is contained in:
2026-06-23 14:51:26 -04:00
parent e36159d25e
commit 4e3d5bbebc

View File

@@ -17,9 +17,22 @@ presentational component tree with a `utils/` library. Zero behavior change.
- No new features, no new filters, no new columns.
- No state-management library (Redux/Zustand) — `useState`/`useMemo` in `App`.
- No test framework installation. Verification is `npm run build` + manual smoke.
- No changes to `main.jsx`, `index.html`, `vite.config.js`, `package.json`,
`Dockerfile`, `docker-compose.yml`, or any of the data-build Python scripts.
- No E2E browser automation (Playwright/Cypress) — component-level UI tests are
the right granularity for a single-page read-only app with no backend.
- No changes to `main.jsx`, `index.html`, `vite.config.js` (beyond adding Vitest
config), Dockerfile, `docker-compose.yml`, or any of the data-build Python
scripts.
## Tech Stack (tests)
- **Test framework:** Vitest + `@testing-library/react` + `@testing-library/jest-dom`
+ `jsdom` + `@testing-library/user-event`. Vite-native, configured via a
`test` block in `vite.config.js` (no separate config file). All
devDependencies — ship no code to production.
- **Coverage target:** not a number — every behavior bullet under `Verification`
step 3 has a corresponding test. The component tests catch single-component
regressions; the App test catches wiring regressions (URL state, derived
data, modal open/close).
## Target File Tree
@@ -41,6 +54,17 @@ react-app/src/
movers.js ← computeMovers(filtered) -> { drops, rises }
columns.jsx ← buildColumns({ isMobile, showFactionCol, onSelectSize })
history.js ← buildChartGeometry(history, dims) -> geometry
test/
setup.js ← vitest setup (imports jest-dom matchers)
fixtures/
data.json ← minimal mock dataset for tests
App.test.jsx ← end-to-end UI tests of the full App
FilterBar.test.jsx ← search + faction + change select behavior
UnitTable.test.jsx ← DataGrid renders rows; size cell click
MoversSection.test.jsx ← drops + rises render; click opens modal
GraphModal.test.jsx ← modal opens with history; size dropdown
SizeCell.test.jsx ← single vs multi-size rendering + click
utils.test.js ← smoke tests for format/url/movers/history
```
## Module Specs
@@ -231,24 +255,82 @@ unchanged.
## Verification
1. `cd react-app && npm run build` — must succeed. Catches circular imports,
1. `cd react-app && npm test` — full Vitest suite must pass. This is the
primary verification for the refactor. Tests cover every UI behavior
listed in the manual smoke checklist below.
2. `cd react-app && npm run build` — must succeed. Catches circular imports,
missing exports, JSX syntax errors, broken path aliases.
2. `cd react-app && npm run dev` and manually verify, in order:
3. `cd react-app && npm run dev` and manually verify, in order (these mirror
the test cases; the tests are the source of truth, this is a final
eyeball check):
a. Page loads, data populates, "Loading…" disappears.
b. Search field filters by name/faction_name/size (case-insensitive).
c. Faction select filters to one faction (Faction column disappears).
d. Change select filters by direction (up/down/no-change/new-only/old-only).
e. URL query string updates as any filter changes; reload restores state.
f. Click a `#` cell with multiple sizes → row's Old/New/Δ% updates to the
selected size's values; click the same cell again to switch back.
f. Click a `#` cell with multiple sizes → row's Old/New/Δ% updates.
g. Click any other cell → modal opens with the correct unit's history.
h. Modal size dropdown switches the chart's history when the unit has
multiple sizes.
h. Modal size dropdown switches the chart's history when applicable.
i. Empty-history units render the "No historical data" message.
j. Movers cards reflect the currently filtered set; clicking a mover row
opens the same modal.
3. `git diff --stat` — should show the new files plus a much smaller
`App.jsx`. No file outside `react-app/src/` should change.
4. `git diff --stat` — should show the new files plus a much smaller
`App.jsx`. No file outside `react-app/src/` (plus the `package.json`
devDeps and the `vitest` config block in `vite.config.js`) should change.
## Test Strategy
**Framework:** Vitest + `@testing-library/react` + `@testing-library/jest-dom`
+ `jsdom` + `@testing-library/user-event`. Configured in `vite.config.js` via
a `test:` block (no separate config file). All test code lives under
`react-app/src/test/`.
**Test categories:**
1. **Pure-function smoke tests** (`test/utils.test.js`) — one or two assertions
per helper in `utils/format.js`, `utils/url.js`, `utils/movers.js`,
`utils/history.js`. No DOM, no React. Fast, deterministic, document the
contracts the components rely on.
2. **Component tests** (`test/FilterBar.test.jsx`, `UnitTable.test.jsx`,
`MoversSection.test.jsx`, `GraphModal.test.jsx`, `SizeCell.test.jsx`) —
render the component in isolation with realistic props (drawn from the
`test/fixtures/data.json` fixture), drive user behavior with
`userEvent`, assert on rendered text / class / role. The handler props
(`onSelect`, `onSelectUnit`, `onCellClick`) are jest `vi.fn()`s and
assertions check they were called with the right args.
3. **End-to-end App test** (`test/App.test.jsx`) — render `<App />` inside
the real `ThemeProvider`, mock `fetch` to return the fixture, then
exercise the full user journey from the README's feature list:
- Page loads → "Loading…" disappears → table rows appear.
- Type into search → rows narrow; clear → rows restore.
- Select a faction → rows filter to that faction; the Faction column disappears.
- Select "↓ Cheaper" → only negative `change_pct` rows remain.
- URL query string reflects the filters (read `window.location.search`).
- Reload page with `?faction=...&dir=...&q=...` → filters rehydrate from URL.
- Click a multi-size `#` cell, choose a different size → row's Old/New/Δ
values update.
- Click any other cell → modal opens with the unit's name and history.
- Click an empty-history unit → "No historical data" appears.
- Click a row in the movers card → same modal opens.
`data-testid` attributes will be added sparingly to `App.jsx` and a couple
of components only where the test needs a stable handle that the
accessible role/text does not provide (e.g. the DataGrid's internal cells,
which don't expose stable roles).
**Mocking strategy:** `globalThis.fetch` is stubbed in each test that needs
data (the App test; component tests use fixture data directly via props).
No other module is mocked — the tests run against the real implementations
of the components and utils. The `ThemeProvider` wrapper is a small helper
in `test/setup.js` to avoid repeating it in every test file.
**Coverage target:** not a number — every behavior bullet under
`Verification` step 3 has a test. The component tests give us confidence
that a one-off regression in a single component is caught; the App test
gives us confidence that the orchestrator (URL state, augmented/filtered
derivation, modal open/close) is wired correctly.
## Migration Strategy
@@ -283,6 +365,19 @@ After each step, `npm run build` confirms nothing is broken mid-refactor.
- **URL effect timing.** The two URL effects in `App` (read on mount, write
on change) move to `App` unchanged. The two helpers in `utils/url.js` only
encapsulate the `window` calls; they don't change the lifecycle.
- **Test data fidelity.** The fixture must cover the variety of cases the
real `data.json` has: at least one unit with multiple sizes, one with no
history, one with `change_pct === 0`, one with `original === null` (new
unit), one with `new === null` (removed unit). Each App test case
references a specific unit from the fixture by name so the test is
self-documenting.
- **DataGrid in jsdom.** `@mui/x-data-grid` works under jsdom but the
internal virtual scroller can be slow. We test by querying by role
(`cell`, `row`) and by visible text. We do not test internal column
resize / sort behavior (out of scope — the app uses none of it).
- **Test runtime.** Vitest in jsdom is fast enough that the full suite
should run in <10s. If a particular test is slow, the fix is to render
smaller fixtures, not to mock React.
## Open Questions