1import { ThreadId } from "@t3tools/contracts";
2import { beforeEach, describe, expect, it } from "vite-plus/test";
3
4import {
5 getThreadKeysToDeselectAfterDelete,
6 useThreadSelectionStore,
7} from "./threadSelectionStore";
8
9const THREAD_A = ThreadId.make("thread-a");
10const THREAD_B = ThreadId.make("thread-b");
11const THREAD_C = ThreadId.make("thread-c");
12const THREAD_D = ThreadId.make("thread-d");
13const THREAD_E = ThreadId.make("thread-e");
14
15const ORDERED = [THREAD_A, THREAD_B, THREAD_C, THREAD_D, THREAD_E] as const;
16
17describe("threadSelectionStore", () => {
18 beforeEach(() => {
19 useThreadSelectionStore.getState().clearSelection();
20 });
21
22 describe("bulk deletion cleanup", () => {
23 it("clears missing selection rows and completed deletions while retaining a failed thread", () => {
24 const store = useThreadSelectionStore.getState();
25 store.toggleThread(THREAD_A);
26 store.toggleThread(THREAD_B);
27 store.toggleThread(THREAD_C);
28 const selected = [...useThreadSelectionStore.getState().selectedThreadKeys];
29 const existingThreads = new Set([THREAD_A, THREAD_B]);
30 store.removeFromSelection(
31 getThreadKeysToDeselectAfterDelete(selected, new Set([THREAD_A]), (key) =>
32 existingThreads.has(ThreadId.make(key)),
33 ),
34 );
35
36 expect([...useThreadSelectionStore.getState().selectedThreadKeys]).toEqual([THREAD_B]);
37 expect(useThreadSelectionStore.getState().anchorThreadKey).toBeNull();
38 });
39
40 it("keeps unprocessed and hidden live threads and selections added while deletion was pending", () => {
41 const store = useThreadSelectionStore.getState();
42 store.toggleThread(THREAD_A);
43 store.toggleThread(THREAD_B);
44 store.toggleThread(THREAD_C);
45 const selected = [...useThreadSelectionStore.getState().selectedThreadKeys];
46 store.toggleThread(THREAD_D);
47 store.removeFromSelection(
48 getThreadKeysToDeselectAfterDelete(selected, new Set([THREAD_A]), () => true),
49 );
50
51 expect([...useThreadSelectionStore.getState().selectedThreadKeys]).toEqual([
52 THREAD_B,
53 THREAD_C,
54 THREAD_D,
55 ]);
56 expect(useThreadSelectionStore.getState().anchorThreadKey).toBe(THREAD_D);
57 });
58 });
59
60 describe("setAnchor", () => {
61 it("enables range select from a plain-click anchor", () => {
62 const store = useThreadSelectionStore.getState();
63 store.setAnchor(THREAD_B);
64 store.rangeSelectTo(THREAD_D, ORDERED);
65
66 const state = useThreadSelectionStore.getState();
67 expect(state.selectedThreadKeys.has(THREAD_B)).toBe(true);
68 expect(state.selectedThreadKeys.has(THREAD_C)).toBe(true);
69 expect(state.selectedThreadKeys.has(THREAD_D)).toBe(true);
70 expect(state.selectedThreadKeys.size).toBe(3);
71 });
72 });
73
74 describe("rangeSelectTo", () => {
75 it("keeps anchor stable across multiple range selects", () => {
76 const store = useThreadSelectionStore.getState();
77 store.toggleThread(THREAD_B);
78 store.rangeSelectTo(THREAD_D, ORDERED);
79 store.rangeSelectTo(THREAD_E, ORDERED);
80
81 const state = useThreadSelectionStore.getState();
82 expect(state.anchorThreadKey).toBe(THREAD_B);
83 expect(state.selectedThreadKeys.has(THREAD_B)).toBe(true);
84 expect(state.selectedThreadKeys.has(THREAD_C)).toBe(true);
85 expect(state.selectedThreadKeys.has(THREAD_D)).toBe(true);
86 expect(state.selectedThreadKeys.has(THREAD_E)).toBe(true);
87 });
88
89 it("falls back to toggle when anchor is not in the ordered list", () => {
90 const store = useThreadSelectionStore.getState();
91 store.toggleThread(THREAD_A);
92 store.rangeSelectTo(THREAD_C, [THREAD_B, THREAD_C, THREAD_D]);
93
94 const state = useThreadSelectionStore.getState();
95 expect(state.selectedThreadKeys.has(THREAD_C)).toBe(true);
96 expect(state.anchorThreadKey).toBe(THREAD_C);
97 });
98
99 it("preserves previously selected threads outside the range", () => {
100 const store = useThreadSelectionStore.getState();
101 store.toggleThread(THREAD_A);
102 store.toggleThread(THREAD_B);
103
104 store.rangeSelectTo(THREAD_D, ORDERED);
105
106 const state = useThreadSelectionStore.getState();
107 expect(state.selectedThreadKeys.has(THREAD_A)).toBe(true);
108 expect(state.selectedThreadKeys.has(THREAD_B)).toBe(true);
109 expect(state.selectedThreadKeys.has(THREAD_C)).toBe(true);
110 expect(state.selectedThreadKeys.has(THREAD_D)).toBe(true);
111 expect(state.selectedThreadKeys.size).toBe(4);
112 });
113 });
114});