The pager is four named arrows round the page box (first, previous, next, last). A table hides a tabletHidden column below 64rem of its own width, as narrowHidden does below 42rem, through one shared helper that also hides the column's <col>. The Channels table drops Prio, About and Length there: at 1024 the channel name had 60 px.
174 lines
6.0 KiB
TypeScript
174 lines
6.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { useLayoutEffect } from "react";
|
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
|
|
vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (k: string) => k }) }));
|
|
|
|
import Pager from "./Pager";
|
|
|
|
afterEach(cleanup);
|
|
|
|
// Rows per page is a short fixed set, so it is the house's segmented control (debt burn 2026-09),
|
|
// not a native <select>: one named group, the current size pressed, "All" as size 0.
|
|
describe("Pager · rows per page", () => {
|
|
it("offers each size and All as one named group, and reports the pick", () => {
|
|
const onSizeChange = vi.fn();
|
|
render(
|
|
<Pager
|
|
page={0}
|
|
size={25}
|
|
total={120}
|
|
pageSizeOptions={[10, 25, 50, 100]}
|
|
onPageChange={() => {}}
|
|
onSizeChange={onSizeChange}
|
|
sizeLabel="Rows per page"
|
|
/>,
|
|
);
|
|
const group = screen.getByRole("group", { name: "Rows per page" });
|
|
expect(group.querySelector('[aria-pressed="true"]')?.textContent).toBe("25");
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "50" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "datatable.all" }));
|
|
expect(onSizeChange.mock.calls).toEqual([[50], [0]]);
|
|
});
|
|
});
|
|
|
|
// The page box DERIVES the current page (E27 S2): it holds a draft only while being edited, so the
|
|
// parent moving the page needs no re-sync effect. The layout-phase probe is what proves "derived"
|
|
// rather than "re-synced after paint" — a passive effect could not have run yet when it reads.
|
|
describe("Pager · the page box follows the page without an effect", () => {
|
|
function Probe({ page, painted }: { page: number; painted: string[] }) {
|
|
useLayoutEffect(() => {
|
|
painted.push(
|
|
(document.querySelector('input[type="number"]') as HTMLInputElement | null)?.value ?? "",
|
|
);
|
|
});
|
|
return (
|
|
<Pager
|
|
page={page}
|
|
size={10}
|
|
total={100}
|
|
pageSizeOptions={[10, 25]}
|
|
onPageChange={() => {}}
|
|
onSizeChange={() => {}}
|
|
sizeLabel="Rows per page"
|
|
/>
|
|
);
|
|
}
|
|
|
|
it("shows the new page in the first frame after the parent moves it", () => {
|
|
const painted: string[] = [];
|
|
const { rerender } = render(<Probe page={0} painted={painted} />);
|
|
expect(screen.getByLabelText("datatable.pager.pageLabel")).toHaveProperty("value", "1");
|
|
|
|
painted.length = 0;
|
|
rerender(<Probe page={4} painted={painted} />);
|
|
expect(painted[0]).toBe("5");
|
|
});
|
|
|
|
it("keeps what you type until you commit it, then follows the page again", () => {
|
|
const onPageChange = vi.fn();
|
|
render(
|
|
<Pager
|
|
page={0}
|
|
size={10}
|
|
total={100}
|
|
pageSizeOptions={[10, 25]}
|
|
onPageChange={onPageChange}
|
|
onSizeChange={() => {}}
|
|
sizeLabel="Rows per page"
|
|
/>,
|
|
);
|
|
const box = screen.getByLabelText("datatable.pager.pageLabel");
|
|
fireEvent.change(box, { target: { value: "7" } });
|
|
expect(box).toHaveProperty("value", "7");
|
|
fireEvent.keyDown(box, { key: "Enter" });
|
|
expect(onPageChange).toHaveBeenCalledWith(6);
|
|
// The parent in this test never moves, so dropping the draft must put page 1 back — proof the
|
|
// box stopped holding its own copy rather than merely being overwritten.
|
|
expect(box).toHaveProperty("value", "1");
|
|
});
|
|
|
|
it("discards an unparseable entry instead of committing it", () => {
|
|
const onPageChange = vi.fn();
|
|
render(
|
|
<Pager
|
|
page={2}
|
|
size={10}
|
|
total={100}
|
|
pageSizeOptions={[10, 25]}
|
|
onPageChange={onPageChange}
|
|
onSizeChange={() => {}}
|
|
sizeLabel="Rows per page"
|
|
/>,
|
|
);
|
|
const box = screen.getByLabelText("datatable.pager.pageLabel");
|
|
fireEvent.change(box, { target: { value: "abc" } });
|
|
fireEvent.blur(box);
|
|
expect(onPageChange).not.toHaveBeenCalled();
|
|
expect(box).toHaveProperty("value", "3");
|
|
});
|
|
});
|
|
|
|
// Glyphs only (the owner, 2026-09-27): First / Previous / Next / Last are four arrows that carry their
|
|
// names as the accessible name and the tooltip, and show no words at any width (the words once pushed
|
|
// a phone row 18px past a 375 screen). The size label stays one line rather than a word per line.
|
|
describe("Pager · four named arrows", () => {
|
|
function renderAt(page: number, onPageChange = vi.fn()) {
|
|
render(
|
|
<Pager
|
|
page={page}
|
|
size={10}
|
|
total={120}
|
|
pageSizeOptions={[10, 25]}
|
|
onPageChange={onPageChange}
|
|
onSizeChange={() => {}}
|
|
sizeLabel="Rows per page"
|
|
/>,
|
|
);
|
|
return onPageChange;
|
|
}
|
|
const NAMES = ["first", "prev", "next", "last"].map((k) => `datatable.pager.${k}`);
|
|
|
|
it("draws each as a glyph with its name as the tooltip, and no visible word", () => {
|
|
renderAt(5);
|
|
for (const name of NAMES) {
|
|
const btn = screen.getByRole("button", { name });
|
|
expect(btn.getAttribute("title")).toBe(name);
|
|
expect(btn.textContent).toBe("");
|
|
expect(btn.querySelector("svg")).not.toBeNull();
|
|
}
|
|
expect(screen.getByText("Rows per page").className).toContain("whitespace-nowrap");
|
|
});
|
|
|
|
it("jumps to the first, previous, next and last page", () => {
|
|
const onPageChange = renderAt(5);
|
|
for (const name of NAMES) fireEvent.click(screen.getByRole("button", { name }));
|
|
expect(onPageChange.mock.calls).toEqual([[0], [4], [6], [11]]);
|
|
});
|
|
|
|
it("turns off the pair that points past an edge", () => {
|
|
renderAt(0);
|
|
const off = (k: string) =>
|
|
(screen.getByRole("button", { name: `datatable.pager.${k}` }) as HTMLButtonElement)
|
|
.disabled ||
|
|
screen.getByRole("button", { name: `datatable.pager.${k}` }).getAttribute("aria-disabled") ===
|
|
"true";
|
|
expect([off("first"), off("prev"), off("next"), off("last")]).toEqual([
|
|
true,
|
|
true,
|
|
false,
|
|
false,
|
|
]);
|
|
cleanup();
|
|
renderAt(11);
|
|
expect([off("first"), off("prev"), off("next"), off("last")]).toEqual([
|
|
false,
|
|
false,
|
|
true,
|
|
true,
|
|
]);
|
|
});
|
|
});
|