Files
siftlode/frontend/src/lib/useCardPager.dom.test.tsx
T
peter 8b2260c9a8 fix(files): review round 1, the page — honest states, the keyboard, the tray's words
- A library past the list's cap is searched and sorted on the server; the count says "the newest".
- Each folder and each search opens on its first page (DataTable pageScope, useCardPager scope).
- A file dropped where the page takes none never reaches the browser (which would unload the app).
- The viewer's arrows wait while a layer opened from it is on top, and scroll a wide text line;
  a text shows its first 5000 lines and says where the rest is.
- Confirms name the file (no more "your downloads"); the asking menu rows end in "…".
- The tray names a running step beside its bar, shows a pause mark for paused work, polls slowly
  while only paused work waits, and forgets a finished file that was deleted.
- An unchanged rename sends nothing; a paste opens the download dialog only with nothing on top;
  the link box never replaces what was typed.
- Honest states: folders that failed to load, a file no longer there, the storage figure; the rail
  says "nothing downloaded" only when there is nothing.
- Focus after deleting the open folder and when the format form opens; the format name's label.
- ui/Meter (one danger line for both quota bars) and ui/RenameField in the platform layer.
- Copy fixes, stale comments and the speculative hub key; a contract test for the list's
  parameters; e2e: the Files page and the paste intake.
2026-10-13 00:14:09 +02:00

124 lines
5.4 KiB
TypeScript

// @vitest-environment jsdom
//
// The card layout's pager clamps an out-of-range page DURING RENDER, 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
});
});
describe("useCardPager — following a moved row", () => {
it("turns to the page that holds the row on each new seq, and not on mount", () => {
const { result, rerender } = renderHook(
({ list, seq }) =>
useCardPager(list, KEY, true, seq ? { seq, match: (r: number) => r === 3 } : null),
{ initialProps: { list: rows(30), seq: 0 } },
);
act(() => result.current.pagerProps.onSizeChange(10));
act(() => result.current.pagerProps.onPageChange(2));
// The row 3 sank to place 25: seq 1 follows it to the third page…
const sunk = [...rows(30).filter((r) => r !== 3)];
sunk.splice(25, 0, 3);
rerender({ list: sunk, seq: 1 });
expect(result.current.pagerProps.page).toBe(2);
// …and bubbled up to place 4: seq 2 follows it back to the first.
rerender({ list: rows(30), seq: 2 });
expect(result.current.pagerProps.page).toBe(0);
// A re-render with the same seq leaves the reader's own paging alone.
act(() => result.current.pagerProps.onPageChange(1));
rerender({ list: rows(30), seq: 2 });
expect(result.current.pagerProps.page).toBe(1);
});
});
describe("useCardPager — another list", () => {
it("starts on page one when the caller's scope changes", () => {
const { result, rerender } = renderHook(
({ scope }) => useCardPager(rows(100), KEY, true, null, undefined, scope),
{ initialProps: { scope: "folder 1" } },
);
act(() => result.current.pagerProps.onPageChange(3));
expect(result.current.pagerProps.page).toBe(3);
rerender({ scope: "folder 2" });
expect(result.current.pagerProps.page).toBe(0);
});
});