Testing
406 tests across four layers. Each exists because the layer below it cannot see the thing it checks.
| Layer | Count | Runs in | What only it can catch |
|---|---|---|---|
| Core unit | 210 | Node | Pure logic: mapping, malformed data, sort order, selection maths, the keyboard contract. |
| React component | 72 | jsdom | Rendering, props, events, ARIA structure, axe violations. |
| Angular component | 56 | jsdom | The same list, against the other renderer — the parity check. |
| End-to-end | 68 | Chromium | Anything needing layout: pixel widths, overflow, truncation, hit-testing, real pointer input, computed styles. |
pnpm test # unit suites (core, react, angular)
pnpm -C e2e test # end-to-end, both renderers
pnpm -C e2e test:headed # watch it happenWhy end-to-end tests exist here
jsdom has no layout engine. Every element measures zero, nothing overflows, and elementFromPoint is meaningless. A whole class of bug is therefore invisible to the unit suites — and two shipped bugs were found exactly this way:
The resize handle was unhittable. It sits in the gap between columns, and the header cell clipped it with overflow: hidden. Every unit test passed, because dispatching an event straight at an element bypasses hit-testing entirely. A real mouse never reached it. The browser test failed with an empty event log, which is what exposed it.
Shift-clicking a checkbox undid its own range. The click extended the selection correctly, then the checkbox's change event fired and toggled the target back off — preventDefault() does not reliably suppress it. The selection sequence was [a] → [a,b,c] → [a,b]. Only an interaction test at that fidelity shows the third step.
Both now have permanent regression tests.
The suite runs against a build, not a dev server
vite dev and ng serve both answer HTTP 200 as soon as they are listening — the signal Playwright waits for — while the bundle is still compiling behind them. The suite therefore builds each example first and serves the output, so "the port is open" genuinely means "the app is ready".
That change also turned a broken build into a build error rather than a screenful of unexplained timeouts, which is how a real bug surfaced: a duplicated @angular/core that broke every @if / @for / @switch block in the built app while ng serve worked fine.
What the end-to-end suite covers
Good paths — navigation and breadcrumbs, folder-scoped and global search, sorting in both directions, single/multi/range selection, built-in and custom actions, list/grid/tree, dark mode, loading, error and empty states, keyboard navigation with type-ahead, the avatar tooltip actually appearing on hover.
Worst cases — these are labelled WORST CASE: in the specs:
- Over-dragging a column until the layout overflows, asserting the row still spans the full scroll width so its hover highlight and borders do not stop short.
- Dragging far enough to crush a flexible column, asserting it holds a readable floor instead of vanishing.
- Dragging hard against the minimum and against a
maxWidth. - Repeated drags in both directions, asserting the layout stays coherent.
- A 320px viewport, asserting nothing scrolls sideways.
- A long owner name, asserting it ellipses rather than forcing the column open.
- Every cell staying inside its own row.
- Recovering from the error state back to content.
Parity
The Angular suites — 56 unit and 9 end-to-end — assert the same outcomes as the React ones against the same fixture. Both renderers drive one engine, so a divergence between the two files means a bug in @document-explorer/core, not in a renderer. That is the point of writing them twice.
The fixture
Every suite loads the same deliberately awkward payload: snake_case keys, numeric ids, a nested children block, an orphan whose parent does not exist, and a duplicate id. Tests assert that the duplicate is dropped and the orphan rescued — so the data-repair behaviour is exercised on every run rather than tested once in isolation.
Coverage
Core sits at 95% statements / 98% lines, enforced by a threshold in CI. The renderers are not coverage-gated: a percentage there measures how much markup a test touched, not whether the component behaves, and the parity and end-to-end suites are the real check.
Accessibility
axe runs against list, grid, tree, empty and error states in the React unit suite. Automated checks catch roughly a third of real accessibility problems, so the end-to-end suite additionally drives the whole thing by keyboard and asserts the roles, the roving tabindex, the live region and the sorted-column announcement.