- 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.
124 lines
5.4 KiB
TypeScript
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);
|
|
});
|
|
});
|