Files
kandev/apps/web/hooks/use-task-multi-select-range.test.ts
José Almeida 5105ac224c feat: cmd+click and shift+click multi-select for kanban and sidebar (#1518)
* feat: add anchored range selection to multi-select reducer

* feat: cmd+click and shift+click multi-select on kanban board

* feat: add cmd/shift multi-select to sidebar task list

* test: e2e coverage for cmd/shift multi-select

* fix: harden multi-select selection lifecycle from review feedback

* test: scope multi-select e2e helpers to active containers

* fix: add sidebar multi-select hook tests and tighten bulk-action lifecycle

* fix: prune hidden sidebar selections and order bulk moves by view

* test: cover visible-order bulk move and anchor realignment

* fix: harden sidebar bulk-move scope and keyboard selection parity

* fix: order kanban bulk moves by board position

* test: cover multi-select helpers and fix stale-id sort fallback

* fix: order kanban card context-menu moves by board position

* feat: sidebar multi-select shows only bulk-valid actions with bulk pin and delete

* fix: harden multi-select bulk ordering, selection clearing, and a11y

* fix: gate kanban card move-to-step for mixed-workflow selections

* fix: unpin fully pinned sidebar selections

* refactor: keep sidebar task components under limits

* test: restore sidebar selection console spy safely
2026-07-06 17:16:14 +01:00

101 lines
4.2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { multiSelectReducer, INITIAL_STATE } from "./use-task-multi-select";
describe("multiSelectReducer — anchor + range", () => {
const ordered = ["a", "b", "c", "d", "e"];
it("toggle_select sets the range anchor to the toggled task", () => {
const next = multiSelectReducer(INITIAL_STATE, { type: "toggle_select", taskId: "t1" });
expect(next.anchorId).toBe("t1");
});
it("select_range with no anchor selects only the clicked task and anchors it", () => {
const next = multiSelectReducer(INITIAL_STATE, {
type: "select_range",
taskId: "c",
orderedIds: ordered,
});
expect(next.selectedIds).toEqual(new Set(["c"]));
expect(next.anchorId).toBe("c");
});
it("selects the inclusive forward range from the anchor", () => {
const anchored = multiSelectReducer(INITIAL_STATE, { type: "toggle_select", taskId: "b" });
const next = multiSelectReducer(anchored, {
type: "select_range",
taskId: "d",
orderedIds: ordered,
});
expect(next.selectedIds).toEqual(new Set(["b", "c", "d"]));
expect(next.anchorId).toBe("b");
});
it("selects the inclusive backward range from the anchor", () => {
const anchored = multiSelectReducer(INITIAL_STATE, { type: "toggle_select", taskId: "d" });
const next = multiSelectReducer(anchored, {
type: "select_range",
taskId: "b",
orderedIds: ordered,
});
expect(next.selectedIds).toEqual(new Set(["b", "c", "d"]));
});
it("unions the range with the existing selection", () => {
const withPrev = { ...INITIAL_STATE, selectedIds: new Set(["x"]), anchorId: "a" };
const next = multiSelectReducer(withPrev, {
type: "select_range",
taskId: "c",
orderedIds: ordered,
});
expect(next.selectedIds).toEqual(new Set(["x", "a", "b", "c"]));
});
it("unions with the existing selection and re-anchors when the anchor is in another column", () => {
const anchored = { ...INITIAL_STATE, selectedIds: new Set(["z"]), anchorId: "z" };
const next = multiSelectReducer(anchored, {
type: "select_range",
taskId: "c",
orderedIds: ordered,
});
expect(next.selectedIds).toEqual(new Set(["z", "c"]));
expect(next.anchorId).toBe("c");
});
it("toggling off the only selected task clears the anchor", () => {
const one = multiSelectReducer(INITIAL_STATE, { type: "toggle_select", taskId: "a" });
expect(one.anchorId).toBe("a");
const empty = multiSelectReducer(one, { type: "toggle_select", taskId: "a" });
expect(empty.selectedIds.size).toBe(0);
expect(empty.anchorId).toBeNull();
});
it("toggling off the anchor realigns it to a remaining selected task", () => {
let s = multiSelectReducer(INITIAL_STATE, { type: "toggle_select", taskId: "a" });
s = multiSelectReducer(s, { type: "toggle_select", taskId: "b" });
// anchor is now 'b'; remove it and the anchor must move to the surviving 'a'.
const next = multiSelectReducer(s, { type: "toggle_select", taskId: "b" });
expect(next.selectedIds).toEqual(new Set(["a"]));
expect(next.anchorId).toBe("a");
});
it("clearing the selection (set_selected empty) drops the anchor", () => {
const dirty = { ...INITIAL_STATE, selectedIds: new Set(["a"]), anchorId: "a" };
const next = multiSelectReducer(dirty, { type: "set_selected", ids: new Set<string>() });
expect(next.anchorId).toBeNull();
});
it("set_selected keeps the anchor when it survives the replacement", () => {
const state = { ...INITIAL_STATE, selectedIds: new Set(["a", "b", "c"]), anchorId: "b" };
const next = multiSelectReducer(state, { type: "set_selected", ids: new Set(["b", "c"]) });
expect(next.anchorId).toBe("b");
});
it("set_selected realigns the anchor when it is dropped (partial bulk failure)", () => {
const state = { ...INITIAL_STATE, selectedIds: new Set(["a", "b", "c"]), anchorId: "a" };
// 'a' archived away; only the failed ids remain — anchor must point at one.
const next = multiSelectReducer(state, { type: "set_selected", ids: new Set(["b", "c"]) });
expect(next.anchorId).not.toBeNull();
expect(next.selectedIds.has(next.anchorId!)).toBe(true);
});
});