While rows are selected the toolbar's second row is the band: the count, the last target as one click, Move to, Remove, Clear selection. Both write in one request and undo back to the places; a video the target held before stays there. Delete takes the selection when there is one. A failure puts the rows back ticked. A row's own move goes through the same write, so it undoes too, and the single-row move route and service function are gone. A playlist row drops its grip below a 24rem table: the row menu keeps the moves.
176 lines
6.8 KiB
TypeScript
176 lines
6.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { describe, it, expect } from "vitest";
|
|
import { act, renderHook } from "@testing-library/react";
|
|
import { useRowSelection } from "./useRowSelection";
|
|
|
|
const ABCDE = ["a", "b", "c", "d", "e"];
|
|
|
|
function setup(shown = ABCDE, all = ABCDE, scope: unknown = 1) {
|
|
return renderHook(
|
|
(p: { all: string[]; shown: string[]; scope: unknown }) =>
|
|
useRowSelection(p.all, p.shown, p.scope),
|
|
{ initialProps: { all, shown, scope } },
|
|
);
|
|
}
|
|
|
|
describe("useRowSelection", () => {
|
|
it("toggles one row at a time, and says the selected rows in the order shown", () => {
|
|
const { result } = setup();
|
|
act(() => result.current.toggle("d"));
|
|
act(() => result.current.toggle("b"));
|
|
expect(result.current.shownSelected).toEqual(["b", "d"]);
|
|
act(() => result.current.toggle("d"));
|
|
expect(result.current.shownSelected).toEqual(["b"]);
|
|
expect(result.current.isSelected("b")).toBe(true);
|
|
expect(result.current.isSelected("d")).toBe(false);
|
|
});
|
|
|
|
it("selects the range from the last row toggled, in either direction", () => {
|
|
const { result } = setup();
|
|
act(() => result.current.toggle("b"));
|
|
act(() => result.current.toggle("d", true));
|
|
expect(result.current.shownSelected).toEqual(["b", "c", "d"]);
|
|
act(() => {
|
|
result.current.clear();
|
|
});
|
|
act(() => result.current.toggle("d"));
|
|
act(() => result.current.toggle("b", true));
|
|
expect(result.current.shownSelected).toEqual(["b", "c", "d"]);
|
|
});
|
|
|
|
it("takes a range's state from the row the range ends on: it can clear rows too", () => {
|
|
const { result } = setup();
|
|
act(() => result.current.toggle("a"));
|
|
act(() => result.current.toggle("e", true));
|
|
expect(result.current.shownSelected).toEqual(ABCDE);
|
|
// `e` is selected, so a range ending on it clears; the anchor moved to `e` with the last click.
|
|
act(() => result.current.toggle("b", true));
|
|
expect(result.current.shownSelected).toEqual(["a"]);
|
|
});
|
|
|
|
it("leaves rows outside the range alone", () => {
|
|
const { result } = setup();
|
|
act(() => result.current.toggle("a"));
|
|
act(() => result.current.toggle("c"));
|
|
act(() => result.current.toggle("e", true));
|
|
expect(result.current.shownSelected).toEqual(["a", "c", "d", "e"]);
|
|
});
|
|
|
|
it("follows the order on screen, not the stored one", () => {
|
|
const { result } = setup(["e", "c", "a", "b", "d"]);
|
|
act(() => result.current.toggle("c"));
|
|
act(() => result.current.toggle("b", true));
|
|
expect(result.current.shownSelected).toEqual(["c", "a", "b"]);
|
|
});
|
|
|
|
it("makes a range with no row to start from a plain toggle", () => {
|
|
const { result } = setup();
|
|
act(() => result.current.toggle("c", true));
|
|
expect(result.current.shownSelected).toEqual(["c"]);
|
|
});
|
|
|
|
it("makes a range whose first row a filter hid a plain toggle", () => {
|
|
const { result, rerender } = setup();
|
|
act(() => result.current.toggle("a"));
|
|
rerender({ all: ABCDE, shown: ["c", "d", "e"], scope: 1 });
|
|
act(() => result.current.toggle("e", true));
|
|
expect(result.current.shownSelected).toEqual(["e"]);
|
|
});
|
|
|
|
it("has the header box cover the rows shown, and keep what a filter hides", () => {
|
|
const { result, rerender } = setup();
|
|
expect(result.current.headerState).toBe("none");
|
|
act(() => result.current.toggle("a"));
|
|
expect(result.current.headerState).toBe("some");
|
|
rerender({ all: ABCDE, shown: ["c", "d"], scope: 1 });
|
|
// The one selected row is hidden: nothing SHOWN is selected, and acting counts what is shown.
|
|
expect(result.current.headerState).toBe("none");
|
|
expect(result.current.shownSelected).toEqual([]);
|
|
act(() => result.current.toggleAllShown());
|
|
expect(result.current.headerState).toBe("all");
|
|
expect(result.current.shownSelected).toEqual(["c", "d"]);
|
|
act(() => result.current.toggleAllShown());
|
|
expect(result.current.headerState).toBe("none");
|
|
rerender({ all: ABCDE, shown: ABCDE, scope: 1 });
|
|
expect(result.current.shownSelected).toEqual(["a"]);
|
|
});
|
|
|
|
it("selects every row shown from a partial selection", () => {
|
|
const { result } = setup();
|
|
act(() => result.current.toggle("b"));
|
|
act(() => result.current.toggleAllShown());
|
|
expect(result.current.shownSelected).toEqual(ABCDE);
|
|
});
|
|
|
|
it("has no header state to show over no rows", () => {
|
|
const { result } = setup([], []);
|
|
expect(result.current.headerState).toBe("none");
|
|
act(() => result.current.toggleAllShown());
|
|
expect(result.current.headerState).toBe("none");
|
|
});
|
|
|
|
it("drops a row that left the list, so it does not come back selected", () => {
|
|
const { result, rerender } = setup();
|
|
act(() => result.current.toggle("b"));
|
|
act(() => result.current.toggle("c"));
|
|
rerender({ all: ["a", "c", "d", "e"], shown: ["a", "c", "d", "e"], scope: 1 });
|
|
expect(result.current.shownSelected).toEqual(["c"]);
|
|
rerender({ all: ABCDE, shown: ABCDE, scope: 1 });
|
|
expect(result.current.shownSelected).toEqual(["c"]);
|
|
});
|
|
|
|
it("clears on a change of scope, in the render that changes it", () => {
|
|
const { result, rerender } = setup();
|
|
act(() => result.current.toggle("a"));
|
|
// The same videos in another list are not selected there.
|
|
rerender({ all: ABCDE, shown: ABCDE, scope: 2 });
|
|
expect(result.current.shownSelected).toEqual([]);
|
|
act(() => result.current.toggle("c", true));
|
|
expect(result.current.shownSelected).toEqual(["c"]);
|
|
});
|
|
|
|
it("counts the selected rows a filter hides", () => {
|
|
const { result, rerender } = setup();
|
|
act(() => result.current.toggle("a"));
|
|
act(() => result.current.toggle("c"));
|
|
expect(result.current.hiddenCount).toBe(0);
|
|
rerender({ all: ABCDE, shown: ["c", "d"], scope: 1 });
|
|
expect(result.current.hiddenCount).toBe(1);
|
|
});
|
|
|
|
it("ticks rows again on top of what is selected by then", () => {
|
|
const { result } = setup();
|
|
act(() => result.current.toggle("e"));
|
|
act(() => result.current.select(["a", "b"]));
|
|
expect(result.current.shownSelected).toEqual(["a", "b", "e"]);
|
|
});
|
|
|
|
it("says whether clearing did anything", () => {
|
|
const { result } = setup();
|
|
let cleared = true;
|
|
act(() => {
|
|
cleared = result.current.clear();
|
|
});
|
|
expect(cleared).toBe(false);
|
|
act(() => result.current.toggle("a"));
|
|
act(() => {
|
|
cleared = result.current.clear();
|
|
});
|
|
expect(cleared).toBe(true);
|
|
expect(result.current.shownSelected).toEqual([]);
|
|
});
|
|
|
|
it("counts a selected row a filter hides as something to clear", () => {
|
|
const { result, rerender } = setup();
|
|
act(() => result.current.toggle("a"));
|
|
rerender({ all: ABCDE, shown: ["c"], scope: 1 });
|
|
let cleared = false;
|
|
act(() => {
|
|
cleared = result.current.clear();
|
|
});
|
|
expect(cleared).toBe(true);
|
|
rerender({ all: ABCDE, shown: ABCDE, scope: 1 });
|
|
expect(result.current.shownSelected).toEqual([]);
|
|
});
|
|
});
|