Enables 14 rules at error with zero violations, so the vacuous-assertion class
the previous commit fixed cannot come back. No budget file and no suppressions
baseline: a rule is on only if it is already at zero.
prefer-to-have-value stays off. It matches any attribute whose name contains
"value", so it rewrites toHaveAttribute("aria-valuenow", n) into toHaveValue(n),
and jest-dom's toHaveValue supports only form controls, which breaks every
role="meter" element the dashboard renders.
Records the enabled set, the rules left off with their measured counts, and the
seven ways these plugins' autofixers produce broken output.
5 KiB
Never put LiteLLM tokens or API keys in localStorage. localStorage survives browser close. Prefer httpOnly cookies, or sessionStorage at most, understanding that any web storage is readable by injected scripts (XSS), and only httpOnly cookies are not
When you fix lint violations that are grandfathered in eslint-suppressions.json, run eslint . --prune-suppressions and commit the updated baseline so the gate ratchets down instead of leaving a stale suppression
src/lib/http/schema.d.ts is generated from the proxy's OpenAPI spec; never hand-edit it. After changing a backend route or response model that the dashboard consumes, run npm run gen:api and commit the result (CI Check UI API Types Sync enforces this)
Tests come in three tiers, named by the standard definitions. Foo.test.tsx is a unit test: one module, collaborators replaced by doubles, no multi-component tree, and it should run in milliseconds. Foo.integration.test.tsx renders a real component tree with real children and only stubs the network boundary; it costs seconds per case, so it earns its place by proving wiring that a unit test cannot reach. Browser-level tests live in tests/e2e/ui/ as Playwright specs against a live proxy
When a component holds logic worth asserting, extract the logic and unit-test it there rather than driving it through a render. CreateMCPServer is the worked example: its payload building lives in createServerPayload.ts with 46 unit tests that run in single-digit milliseconds, while CreateMCPServer.integration.test.tsx keeps only the cases that prove a form field reaches the right payload key. A test that renders a whole modal to assert the shape of one object belongs in the first category, not the second
Most of the suite predates this split and is not yet classified, so an unsuffixed *.test.tsx is not evidence that a file is really a unit test. Classify what you touch
Assert something the user could perceive, and assert it precisely enough that the test fails when the behaviour breaks. eslint-plugin-testing-library and eslint-plugin-jest-dom enforce the mechanical part of that. Two of the enabled rules exist because the failure they catch is silent rather than cosmetic: await-async-queries catches an unawaited findBy*, whose returned Promise is always truthy and makes the whole assertion vacuous, and no-wait-for-side-effects catches work inside a waitFor callback, which is retried on every poll. Prefer findBy* over waitFor wrapped around getBy*, and keep a waitFor callback to a single assertion
Do not trust eslint --fix for these two plugins. Fixing the suite in bulk produced seven distinct kinds of broken output. Four fail loudly: no-wait-for-side-effects and no-wait-for-multiple-assertions hoist a statement out of the waitFor callback while leaving the const it reads inside, prefer-enabled-disabled drops a closing paren when the subject carries a type assertion, prefer-presence-queries swaps in a query it never destructures, and prefer-in-document collapses getAllBy* to getBy* on a value still indexed as an array. Two fail quietly, which is worse: prefer-checked swaps the checked attribute for the .checked property, and antd radios set one without the other, and prefer-to-have-text-content wraps arbitrary strings in new RegExp() without escaping, so toContain("100K+ requests") becomes a pattern meaning "100 followed by one-or-more K". That last one compiles, lints clean, and keeps passing while no longer asserting what it says. Pass a plain string to toHaveTextContent, which is already a substring match. Run the fixer on a handful of files at a time and read the diff
jest-dom/prefer-to-have-value stays off because its fixer is wrong here, not merely noisy. It matches any attribute whose name contains "value", so it rewrites toHaveAttribute("aria-valuenow", n) into toHaveValue(n), and jest-dom's toHaveValue only supports form controls, so the assertion fails on the role="meter" elements the dashboard renders. Assert ARIA value attributes with toHaveAttribute
A test may reach for a component library's own CSS class only when that library exposes no role, label, title or ARIA state to query instead, and then the line carries a suppression naming the rule and the reason. Check first: antd icons render as role="img" with an aria-label, and antd Form.Item associates its label with the control, so both are reachable accessibly. When a label does not resolve, suspect the control rather than the test, since a custom wrapper that destructures props without spreading them drops the id antd injects and leaves the rendered label pointing at nothing
Rules beyond the enabled set were measured against the whole suite and left off rather than recorded in a budget file, because a ceiling that permits a violation anywhere is worse than an honest gap. no-node-access and no-container are the ones worth revisiting first, since they catch the DOM archaeology the rules above only discourage. prefer-implicit-assert and prefer-explicit-assert contradict each other, so neither is enabled