build(frontend): finish the React 19 move, and hold the versions that were left in prose

react-i18next was still on ^14.1.2. The E27 S1 plan named it as "the one peer that
actually needs moving — React 19 support is the 15 line", and then it did not
move. The 14 line's <Trans> reads `element.ref`, removed in React 19, and its
withTranslation types are built on the GLOBAL JSX namespace @types/react 19 no
longer declares — both latent only because this app imports neither. 15.7.4 fixes
both at the source (`c.props.ref ?? c.ref`, `React.JSX.LibraryManagedAttributes`).

`reactEngine.contract.test.ts` joins its Node and Python twins and holds what was
only a comment: the compiler's `target` against the installed React major, react
and react-dom together, the types against the runtime, and react-i18next's floor.
A stale target is not a corrupt bundle — the plugin validates the option — but it
is a silent one, since tsc never sees Babel's output.

pythonEngine gains the seventh spelling nobody held: `requirements.lock`'s own
regeneration recipe names a python image, and wheels resolve against THAT
interpreter, so a recipe left behind yields a lock that does not match the image
installing it.

The type lane now covers `alembic` too, for the reason the ruff lane was widened
to it — migrations run on every container start, so one that raises crash-loops
prod. It came back at zero new errors, so the baseline is untouched. `tests/` is
deliberately still out, and the config says why.

vitest.config's docblock claimed the React transform is "a no-op for the plain .ts
node tests". It is not: the compiler rewrites src/lib's hooks in that lane too,
which is the point of sharing one factory with the build — and worth knowing
before ruling the compiler out of a divergence.

Tests: useCardPager had none, in either the hook or its two callers, though its
page clamp moved to render in S2 — the transformation where a non-convergent
condition is a crash rather than a glitch. Four cases now, including termination
on an empty list. Combobox's and Avatar's close/failed-URL cases are relabelled
CHARACTERIZATION: both pass against the pre-sweep code, and the tests beside them
are the ones that discriminate.
This commit is contained in:
2026-09-21 23:33:02 +02:00
parent 05121fbc99
commit 378b7559e4
9 changed files with 202 additions and 15 deletions
@@ -247,10 +247,12 @@ describe("Combobox", () => {
expect(screen.getByRole("combobox").getAttribute("aria-expanded")).toBe("false"); // closed
});
// The typed query is discarded when the list closes, and it happens IN the close — the clear used
// to be an effect on `open`, which fired a render later and, in controlled mode, reported the
// empty query outside any event (E27 S2). The list is for "find and pick": a leftover filter would
// hide everything worth seeing the next time it opens.
// The typed query is discarded when the list closes. CHARACTERIZATION, not the regression guard:
// the old effect-based clear satisfies these assertions too, because RTL wraps fireEvent in act()
// and the passive effect has flushed by the time the value is read. What the move into the close
// funnel actually fixed is measured by the next test, which the old shape fails. This one is here
// because the behaviour is worth pinning: the list is for "find and pick", and a leftover filter
// would hide everything worth seeing the next time it opens.
it("discards the typed query when the list closes, so the next open shows everything", () => {
wrap(<Combobox label="Genre" options={OPTIONS} selected={[]} onChange={vi.fn()} />);
const input = screen.getByRole("combobox");