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.
87 lines
3.8 KiB
TypeScript
87 lines
3.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The card layout's pager clamps an out-of-range page DURING RENDER (E27 S2), which is the right
|
|
// shape — React discards that pass, so nothing is committed on a page that no longer exists — but
|
|
// it is also the shape that turns a non-convergent condition into "Too many re-renders" rather than
|
|
// a visual glitch. Neither the hook nor either of its two callers (ChannelDiscovery, Channels) had
|
|
// a test, so nothing held either half. This file holds both: the clamp happens, and it terminates.
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { renderHook, act } from "@testing-library/react";
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return { ...actual, useTranslation: () => ({ t: (k: string) => k }) };
|
|
});
|
|
|
|
import { useCardPager } from "./useCardPager";
|
|
|
|
const rows = (n: number) => Array.from({ length: n }, (_, i) => i);
|
|
const KEY = "siftlode.test.cardSize";
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
sessionStorage.setItem("siftlode.activeAccount", "1");
|
|
});
|
|
|
|
describe("useCardPager — an out-of-range page", () => {
|
|
it("clamps to the last page that still exists when the list shrinks", () => {
|
|
const { result, rerender } = renderHook(({ n }) => useCardPager(rows(n), KEY, true), {
|
|
initialProps: { n: 30 },
|
|
});
|
|
act(() => result.current.pagerProps.onSizeChange(10));
|
|
act(() => result.current.pagerProps.onPageChange(2));
|
|
expect(result.current.pagerProps.page).toBe(2);
|
|
expect(result.current.paged).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]);
|
|
|
|
rerender({ n: 5 }); // an unsubscribe, a refetch — page 2 no longer exists
|
|
|
|
expect(result.current.pagerProps.page).toBe(0);
|
|
expect(result.current.paged).toEqual([0, 1, 2, 3, 4]);
|
|
});
|
|
|
|
it("does not spring back to the stale page when the list grows again", () => {
|
|
const { result, rerender } = renderHook(({ n }) => useCardPager(rows(n), KEY, true), {
|
|
initialProps: { n: 30 },
|
|
});
|
|
act(() => result.current.pagerProps.onSizeChange(10));
|
|
act(() => result.current.pagerProps.onPageChange(2));
|
|
|
|
rerender({ n: 5 });
|
|
rerender({ n: 30 });
|
|
|
|
// The clamp was COMMITTED, not merely displayed — `safePage` alone would have let 2 come back.
|
|
expect(result.current.pagerProps.page).toBe(0);
|
|
});
|
|
|
|
it("terminates on an empty list — the write falsifies its own condition", () => {
|
|
const { result, rerender } = renderHook(({ n }) => useCardPager(rows(n), KEY, true), {
|
|
initialProps: { n: 30 },
|
|
});
|
|
act(() => result.current.pagerProps.onSizeChange(10));
|
|
act(() => result.current.pagerProps.onPageChange(2));
|
|
|
|
// totalPages floors at 1, so the clamp target is 0 and the condition is false next pass. If it
|
|
// were not, this line would throw "Too many re-renders" instead of returning.
|
|
rerender({ n: 0 });
|
|
|
|
expect(result.current.pagerProps.page).toBe(0);
|
|
expect(result.current.paged).toEqual([]);
|
|
});
|
|
|
|
it("holds still in table mode, where DataTable does its own paging", () => {
|
|
const { result, rerender } = renderHook(({ n, on }) => useCardPager(rows(n), KEY, on), {
|
|
initialProps: { n: 30, on: false },
|
|
});
|
|
act(() => result.current.pagerProps.onSizeChange(10));
|
|
act(() => result.current.pagerProps.onPageChange(2));
|
|
|
|
rerender({ n: 5, on: false }); // the same shrink — but nothing is committed here
|
|
rerender({ n: 30, on: false });
|
|
|
|
// `safePage` still clamps for DISPLAY while the list is short; what must not happen is the
|
|
// clamp being written, which would lose the page for real. Growing back proves it was not.
|
|
expect(result.current.pagerProps.page).toBe(2);
|
|
expect(result.current.paged).toEqual(rows(30)); // and the rows are never sliced in table mode
|
|
});
|
|
});
|