Files
siftlode/frontend/src/components/ui/OrderButtons.dom.test.tsx
T
peter 4d0abc61d5 feat(design): reorder without a drag everywhere a grip drags (WCAG 2.5.7, E28 S3e4a)
One rule, two forms: useOrderItems (Top / Up / Down / Bottom rows of a row
menu) and OrderButtons (four buttons with the place, or a compact Up / Down
pair). A move that would go nowhere is shown off.

- YouTube playlist rows: the four moves now come from useOrderItems.
- Plex playlist rows: a block among the blocks, an episode within its show,
  off while a write is in flight; drag and menu share moveBlock/moveEpisode.
- Saved views editor: the selected view's four buttons (owner: A), and the
  grip's keyboard pick-up (its instructions promised one it did not have).
- Side panel groups in edit mode: an Up / Down pair (owner: A); a group the
  stored order does not hold yet moves too; the name wraps there.
- The grip's touch-action: none is now test-held.
2026-09-26 18:14:28 +02:00

75 lines
2.9 KiB
TypeScript

// @vitest-environment jsdom
//
// The non-drag reorder (WCAG 2.5.7): the four menu rows and the four buttons, one rule — a move that
// would go nowhere is shown off, and the rest report the index the item lands on.
import { describe, it, expect, vi, afterEach } from "vitest";
import { render, renderHook, screen, fireEvent, cleanup } from "@testing-library/react";
vi.mock("react-i18next", async (importOriginal) => {
const actual = await importOriginal<typeof import("react-i18next")>();
return {
...actual,
useTranslation: () => ({
t: (key: string, o?: { pos?: number; total?: number }) =>
o?.pos !== undefined ? `${key}:${o.pos}/${o.total}` : key,
}),
};
});
import { OrderButtons } from "./OrderButtons";
import { useOrderItems } from "./useOrderItems";
afterEach(cleanup);
describe("useOrderItems", () => {
const rows = (...a: Parameters<ReturnType<typeof useOrderItems>>) =>
renderHook(() => useOrderItems()).result.current(...a);
it("offers top, up, down and bottom, each reporting where the item lands", () => {
const onMove = vi.fn();
const items = rows(2, 5, onMove);
expect(items.map((i) => i.id)).toEqual(["order-top", "order-up", "order-down", "order-bottom"]);
expect(items.map((i) => i.label)).toEqual([
"common.order.top",
"common.order.up",
"common.order.down",
"common.order.bottom",
]);
expect(items[0]!.separatorBefore).toBe(true);
items.forEach((i) => i.onSelect!());
expect(onMove.mock.calls.map((c) => c[0])).toEqual([0, 1, 3, 4]);
});
it("turns off the moves that go nowhere: the first item up, the last down", () => {
expect(rows(0, 3, vi.fn()).map((i) => !!i.disabled)).toEqual([true, true, false, false]);
expect(rows(2, 3, vi.fn()).map((i) => !!i.disabled)).toEqual([false, false, true, true]);
});
it("turns all four off with the reason under the first row only", () => {
const items = rows(1, 3, vi.fn(), { reason: "sorted" });
expect(items.every((i) => i.disabled)).toBe(true);
expect(items.map((i) => ("note" in i ? i.note : undefined))).toEqual([
"sorted",
undefined,
undefined,
undefined,
]);
});
});
describe("OrderButtons", () => {
it("shows the item's place, moves it, and holds the moves that go nowhere off", () => {
const onMove = vi.fn();
render(<OrderButtons from={0} count={3} onMove={onMove} />);
expect(screen.getByRole("group").getAttribute("aria-label")).toBe(
"common.order.label common.order.place:1/3",
);
const btn = (name: string) => screen.getByRole("button", { name }) as HTMLButtonElement;
expect(btn("common.order.top").disabled).toBe(true);
expect(btn("common.order.up").disabled).toBe(true);
fireEvent.click(btn("common.order.down"));
fireEvent.click(btn("common.order.bottom"));
expect(onMove.mock.calls.map((c) => c[0])).toEqual([1, 2]);
});
});