# Quality

> What gates a change to ngwr — 8 checks on every pull request, 4,084 test cases and three accessibility sweeps — and an honest account of what those gates cannot see.

Source: https://ngwr.dev/start/quality  
Kind: Getting started

## Counted, not claimed

Every number on this page is written by `scripts/gen-quality.ts`, which counts the repository during the showcase build. Nothing here is typed in by hand — that is the only way a page like this is still true a year after it was written.

Every counter says what it measured. Test cases are declared `it()` call sites, which equals the real figure only while nothing mints cases at runtime — so a parameterised form (`it.each`) or a call site inside a loop makes the generator print the file and line and label this figure a floor, and the page then reads “at least”. A spec file that yields zero cases still fails the build outright, because that is not a way of writing tests, it is what a broken comment-stripper looks like. “Documentation pages” counts routes that render one: the layout shell and the `**` 404 both declare a component and neither is a page, so neither is in the 208. The version is read from `projects/lib/package.json` and cross-checked against the `NGWR_VERSION` constant, which is a separate copy; a mismatch fails the build rather than picking a side. The single runtime dependency is `tslib`.

## The gates on every pull request

These run on every push to a pull request, in this order. The last two read the prerendered site, so they cannot start before it exists.

All 8 of them have to be green before anything merges, and the publish job re-runs the lint and the library build again before it ships a tarball.

| Gate | What it catches |
| --- | --- |
| `lint` | ESLint, Stylelint and four repository gates in one chain — colour-list parity, unexplained physical CSS, the registry format, dead design tokens. Every stage is listed below. |
| `test:coverage` | The vitest suite, with coverage. Specs sit beside the code they cover and assert the rendered DOM — roles, ARIA state, and the .wr-* classes, which are public API — rather than component internals. |
| `check:api-docs` | A documented input the component no longer has, a default the docs invented, a page with no API table at all. Every hand-written table is compared against the signal API in the source. |
| `check:llms` | The generated AI assets — llms-full.txt and the agent skill — against coverage floors. Missing frontmatter, or a catalog table with nothing but a header, fails the build. |
| `build:lib` | ng-packagr over every secondary entry point, then the schematics, the MCP server and the AI assets. An entry point that does not compile in isolation fails here and nowhere else. |
| `build:showcase` | Every documentation route prerendered in Node. SSR breakage is a red build rather than a silent degrade: a component that touches the DOM outside afterNextRender cannot reach a release. |
| `check:theme` | wrThemeTokens(), the runtime palette recipe, against the compiled stylesheet — token by token. Two implementations of one recipe drift the moment either is edited. |
| `check:a11y` | axe over the prerendered HTML: accessible names, ARIA validity, roles, id references, landmark and heading structure. Fails on any serious or critical violation, and the baseline is empty. |

Trust the exit code, not the output.

pnpm lint is a chain of stages joined by &&, and the first one prints “All files pass linting.” on its way to a later stage that fails. A silently failed lint stage slipped past exactly once, and it blocked a publish.

## Inside pnpm lint

`ng lint` is only the first stage. The rest are gates a linter has no opinion about: they read the repository for the kind of drift that leaves both halves independently valid while the pair is wrong.

| Stage | What it catches |
| --- | --- |
| `ng lint` | ESLint over the library, then the showcase — templates included. |
| `eslint scripts` | The same rules over the build and release tooling — TypeScript that never ships to npm, and that every gate on this page runs through. |
| `lint:styles` | Stylelint over every stylesheet in both projects. |
| `check:colors` | The TypeScript colour list against the SCSS palette map. They drifted once: v8 shipped --wr-color-info and its whole modifier class while color="info" stayed a template type error. |
| `check:rtl` | A direction-dependent CSS property written in physical form with no rtl-ok: reason above it. Plenty of them are correct — the rule is that the reason is written down. |
| `check:registry` | The open registry items, their entryPoints against the real catalog, and schema.json against the validator that enforces it. |
| `check:tokens` | A --wr-* token nothing paints with. A say-why gate rather than a do-not gate: an intentionally unused token carries unused-ok: and the reason. |

## What runs nightly instead

The rest of the sweep needs a real browser, real CSS and several hundred page loads. On the pull-request path that roughly tripled the job, and what it catches is drift in painted output rather than the kind of break a single review has to be told about — so it runs on a schedule instead.

| Gate | What it catches |
| --- | --- |
| `build:showcase` | Not a check. The three below read dist/showcase and cannot start without it. |
| `check:contrast` | axe's color-contrast and target-size rules in a real Chromium, both themes, every canonical route — the two rules check:a11y has to switch off. |
| `check:state-a11y` | The full axe rule set inside a state you have to create: a hover, a focus ring, an open overlay. Neither static gate can reach one. |
| `check:rtl-layout` | Every route rendered both ways, failing only where the RTL pass overflows sideways and the LTR pass does not. Differential, so there is no baseline of pixel positions to rot. |

The trade is worth saying out loud: **a green pull request says nothing about painted contrast or RTL overflow.** If you are working on the library and you touch a token, a tint, or anything that paints text on an intent, run `pnpm check:contrast` yourself rather than waiting for the morning.

## Three accessibility gates, and what each one cannot see

Three sweeps, because no single one of them reaches the whole surface. The last two rows of each block are the ones that matter: what the gate is blind to, and what it has already been told to accept.

### `check:a11y`

### `check:contrast`

### `check:state-a11y`

And one failure no gate reports at all.

--wr-color-outline measures 1.48:1 in light and 1.41:1 in dark, against the 3:1 WCAG 1.4.11 asks of anything that identifies a control. Fifteen of its declarations are control boundaries where the criterion applies; the rest are cards, dividers and table rules, which it does not reach. Both fixes were costed — darkening the one token repaints every rule that uses it, and splitting out a control-border token leaves control edges visibly darker than every line beside them — and the maintainer chose to keep the hairline. No gate reports it, because axe ships no non-text-contrast rule.

## What the tests cover, and what they do not

The suite is broad. It is not uniformly deep, and the shallow parts are worth naming.

4,084 cases across 238 spec files, and 202 of the 202 entry points own at least one. Read that second figure narrowly: it is file-level coverage of the catalog, not branch coverage of the code. Specs sit beside what they cover and assert the **rendered DOM** — roles, ARIA state and the `.wr-*` class names, which are public API because every component ships `ViewEncapsulation.None`. A spec that reaches into component internals passes straight through the kind of change that actually breaks people.

103 CDK test harness classes ship alongside, published from 70 `ngwr/<name>/testing` entry points, so your own tests can drive a select or a date picker without knowing its markup. Four entry points deliberately have none — three that draw into a canvas jsdom hands no context for, and a thin wrapper over the CDK's virtual scroller — and the reason is the same each time: every honest method would answer identically for a working component and a broken one.

Which is also the first hole. **jsdom has no drawing context and no layout** — every rect is 0×0 — so the canvas and WebGL components can never assert anything painted. All six install a recording context and five an error recorder besides, which carries their specs past the early return to frame counts, particle counts, a `var(--wr-…)` colour resolved against the host, gravity bringing a particle back down, and teardown. Painted output itself is not reachable, and neither is anything measured in pixels.

**The second hole is mode coverage inside components that are covered.**`wr-table` has column pinning, drag-reorder, grouping, tree rows, expandable rows, CSV export and a virtualized body on one host; a spec on the table says nothing about tree rows unless it exercises them, and not every spec does. A green suite here means no covered behaviour regressed — not that every behaviour is covered.

**The third is classic-forms interop, and it is the one worth knowing about** if you are weighing ngwr against a library that still ships a `ControlValueAccessor`. Angular 22 synthesises the accessor for a signal-forms control, so `[(ngModel)]` and `[formControl]` reach the same `value` model — but in this suite that is doctrine more than coverage. `[(ngModel)]` is bound to a real ngwr control in exactly one spec (`wr-textarea`), and **no spec binds `[formControl]` to one at all** — the reactive-forms specs that exist drive native `<input>`s through `WrValidators`. Angular's own tests cover the synthesis; nothing here covers it per component.

## Breaking changes, and which ones a codemod can honestly repair

A major ships an `ng update` codemod when a codemod can actually repair what broke — and deliberately ships none when it cannot.

That rule is the whole table. v12 moved three import paths, which is exactly what a codemod can move without reading the code around it. v10 and v11 changed painted colour, which no codemod can repair — and shipping an empty one would have told you your visual regressions were handled when they were not.

| Major | What broke | Codemod |
| --- | --- | --- |
| `v12` | The three date entry points nested under ngwr/date. readI18nText() returns a Signal, so every read needs a call. | Partial |
| `v11` | Five colour intents deepened past the point where a filled control takes a white label instead of a black one. | None |
| `v10` | Contrast on the -contrast tokens, table header casing, tooltip theming. | None |
| `v9` | A checkbox’s group identity moved from value to checkboxValue. Lucide icon keys register verbatim. info joined the colour union. | Yes |
| `v8` | Density values renamed from compact / default / comfortable to sm / md / lg. Pagination dropped xs and xl. Two unreliable components removed. | Yes |
| `v7` | Ten standalone entry points consolidated into shared components with modes — the autocomplete became a wr-select, the tooltip a wr-popover. | Yes |

The partial is deliberate too. v12's other break — `readI18nText()` returning a `Signal<string>` — is not codemodded, because adding `()` means knowing which identifiers hold the result, and a wrong guess is a silent behaviour change. The type error names every call site instead. The [migration guide](https://ngwr.dev/start/migration) documents both halves, and every break back to v7.

## What this does not buy you

All of the above is true, and none of it is the whole picture.

## See also

- [Testing](https://ngwr.dev/guides/testing) — The CDK harnesses, and how to drive ngwr components from your own specs.
- [Migration guide](https://ngwr.dev/start/migration) — Every breaking change back to v7, and which ones the codemod rewrites for you.
- [Colour tokens](https://ngwr.dev/guides/tokens/colors) — The contrast and ink split, and where the ratios quoted above come from.
