Nothing in the dashboard renders antd any more, so the package and the
scaffolding around it can go. This removes `antd` and
`@ant-design/cssinjs` from package.json, deletes the global StyleProvider
the root layout wrapped every page in, drops the `antd` cascade layer and
the z-index override that lifted Base UI popups over an antd Modal, and
retires the lint rules that policed antd imports and antd class selectors
in tests.
Fifteen test files still carried `vi.mock("antd", ...)` factories for
components that stopped importing antd during the migration. They were
inert, and they resolve the real module, so they would have broken the
moment the package left node_modules.
The compatibility shims keep their behaviour and lose the antd name:
`antdRules`/`antdRequired` become `validatorRules`/`requiredRule`,
`isAntdUrl` becomes `isValidUrl`, and `ABOVE_ANTD_MODAL` becomes
`NESTED_DIALOG_LAYER`. Comments that explain why a contract looks the way
it does still name antd, because that history is the reason.
5.9 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 a radio can 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
Reach for fireEvent.change rather than user.type when a test only needs a field to hold a value. user.type dispatches one event per character and re-renders the whole form each time, which is why a single form test could burn seven seconds. Keep user.type where the typing itself is the behaviour under test: an autocomplete that filters per keystroke, a debounce, a key handler, or any Base UI combobox, whose filter state is driven by real keyboard input and does not react to a raw change event
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. Check first: the shadcn primitives forward roles and aria-label, and the shared form field associates its label with the control, so both are reachable accessibly. When nothing accessible identifies the element, prefer its data-slot attribute, which the primitives set deliberately and treat as stable. 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 the field generates 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
Never run the full unit suite (npx vitest run with no path). It is 380 files and thousands of tests, it saturates the machine for many minutes, and CI runs it anyway. Run only the test files your change touches, plus any file whose failure your change could plausibly explain, by passing explicit paths