The per-tab account pin, the X-Siftlode-Account header and ?account= go: the session cookie alone says who is asking. localStorage keys lose their account suffix; the browser records whose state it holds (siftlode.owner), BootGate hands it to the account the server answered as, and a different account starts from a clean slate (the public /watch keys stay). The first boot moves the account's old per-account copies onto the plain keys and drops other accounts' copies. Until the claim, and after another tab claimed the browser for someone else, the account helpers read and write nothing. The account panel lists no other accounts: "Add another account" becomes "Sign in as someone else" (Google's account chooser; the pick replaces this account, a cancel keeps it), shown where Google sign-in is configured.
125 lines
5.4 KiB
TypeScript
125 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";
|
|
import { claimLocalData } from "./storage";
|
|
|
|
const rows = (n: number) => Array.from({ length: n }, (_, i) => i);
|
|
const KEY = "siftlode.test.cardSize";
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
claimLocalData(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);
|
|
});
|
|
});
|