1import { beforeEach, describe, expect, it } from "vite-plus/test";
2
3import { scopeThreadRef } from "@t3tools/client-runtime/environment";
4import { type EnvironmentId, ThreadId } from "@t3tools/contracts";
5
6import {
7 isSameSidebarThreadRef,
8 useSidebarPendingFileDropStore,
9 type SidebarPendingFileDrop,
10} from "./sidebarPendingFileDropStore";
11
12function makeFiles(...names: string[]): File[] {
13 return names.map((name) => new File(["x"], name));
14}
15
16function makeEntry(
17 environmentId: string,
18 threadId: string,
19 files: File[],
20): Omit<SidebarPendingFileDrop, "id"> {
21 return {
22 threadRef: scopeThreadRef(environmentId as EnvironmentId, ThreadId.make(threadId)),
23 files,
24 };
25}
26
27function fileNames(files: File[]): string[] {
28 return files.map((file) => file.name);
29}
30
31function refOf(environmentId: string, threadId: string) {
32 return makeEntry(environmentId, threadId, []).threadRef;
33}
34
35beforeEach(() => {
36 useSidebarPendingFileDropStore.setState({ pending: [] });
37});
38
39describe("sidebarPendingFileDropStore", () => {
40 it("starts empty", () => {
41 expect(useSidebarPendingFileDropStore.getState().pending).toEqual([]);
42 });
43
44 it("stashes and consumes a drop for the matching thread", () => {
45 const files = makeFiles("a.png", "b.png");
46 const store = useSidebarPendingFileDropStore.getState();
47 store.queuePendingFileDrop(makeEntry("env-1", "thread-1", files));
48
49 expect(
50 useSidebarPendingFileDropStore.getState().consumePendingFileDrop(refOf("env-1", "thread-1")),
51 ).toEqual(files);
52 expect(useSidebarPendingFileDropStore.getState().pending).toEqual([]);
53 });
54
55 it("accumulates repeat drops onto the same thread instead of replacing", () => {
56 const store = useSidebarPendingFileDropStore.getState();
57 store.queuePendingFileDrop(makeEntry("env-1", "thread-1", makeFiles("a.png")));
58 store.queuePendingFileDrop(makeEntry("env-1", "thread-1", makeFiles("b.png")));
59
60 expect(
61 fileNames(
62 useSidebarPendingFileDropStore
63 .getState()
64 .consumePendingFileDrop(refOf("env-1", "thread-1")) ?? [],
65 ),
66 ).toEqual(["a.png", "b.png"]);
67 expect(useSidebarPendingFileDropStore.getState().pending).toEqual([]);
68 });
69
70 it("keeps drops for other threads when consuming one thread", () => {
71 const store = useSidebarPendingFileDropStore.getState();
72 store.queuePendingFileDrop(makeEntry("env-1", "thread-1", makeFiles("a.png")));
73 store.queuePendingFileDrop(makeEntry("env-1", "thread-2", makeFiles("b.png")));
74
75 expect(
76 fileNames(
77 useSidebarPendingFileDropStore
78 .getState()
79 .consumePendingFileDrop(refOf("env-1", "thread-2")) ?? [],
80 ),
81 ).toEqual(["b.png"]);
82 expect(useSidebarPendingFileDropStore.getState().pending).toHaveLength(1);
83 });
84
85 it("clears only the drop matching a stale navigation's id", () => {
86 const store = useSidebarPendingFileDropStore.getState();
87 const firstId = store.queuePendingFileDrop(makeEntry("env-1", "thread-1", makeFiles("a.png")));
88 store.queuePendingFileDrop(makeEntry("env-1", "thread-1", makeFiles("b.png")));
89
90 useSidebarPendingFileDropStore.getState().clearPendingFileDrop(firstId);
91 expect(
92 fileNames(
93 useSidebarPendingFileDropStore
94 .getState()
95 .consumePendingFileDrop(refOf("env-1", "thread-1")) ?? [],
96 ),
97 ).toEqual(["b.png"]);
98 });
99
100 it("keeps a newer drop deliverable after the first navigation fails", () => {
101 // Mirrors handleThreadFileDrop: two drops queued for the same unopened
102 // thread, then the first navigation fails (or lands elsewhere) and cleans
103 // up by its own drop id. The newer drop must survive with its files
104 // intact so the thread opening still attaches them.
105 const store = useSidebarPendingFileDropStore.getState();
106 const firstId = store.queuePendingFileDrop(makeEntry("env-1", "thread-1", makeFiles("a.png")));
107 const secondId = store.queuePendingFileDrop(makeEntry("env-1", "thread-1", makeFiles("b.png")));
108
109 // First navigation fails: handler clears only its own drop.
110 useSidebarPendingFileDropStore.getState().clearPendingFileDrop(firstId);
111 const remaining = useSidebarPendingFileDropStore.getState().pending;
112 expect(remaining).toHaveLength(1);
113 expect(remaining[0]?.id).toBe(secondId);
114 expect(fileNames(remaining[0]?.files ?? [])).toEqual(["b.png"]);
115
116 // Thread opens: the surviving drop is still attached.
117 expect(
118 fileNames(
119 useSidebarPendingFileDropStore
120 .getState()
121 .consumePendingFileDrop(refOf("env-1", "thread-1")) ?? [],
122 ),
123 ).toEqual(["b.png"]);
124 expect(useSidebarPendingFileDropStore.getState().pending).toEqual([]);
125 });
126
127 it("clears every drop for a missing thread", () => {
128 const store = useSidebarPendingFileDropStore.getState();
129 store.queuePendingFileDrop(makeEntry("env-1", "thread-1", makeFiles("a.png")));
130 store.queuePendingFileDrop(makeEntry("env-1", "thread-1", makeFiles("b.png")));
131 store.queuePendingFileDrop(makeEntry("env-1", "thread-2", makeFiles("c.png")));
132
133 useSidebarPendingFileDropStore
134 .getState()
135 .clearPendingFileDropsForThread(refOf("env-1", "thread-1"));
136 expect(
137 fileNames(
138 useSidebarPendingFileDropStore
139 .getState()
140 .consumePendingFileDrop(refOf("env-1", "thread-2")) ?? [],
141 ),
142 ).toEqual(["c.png"]);
143 expect(useSidebarPendingFileDropStore.getState().pending).toEqual([]);
144 });
145
146 it("does not confuse refs whose joined keys collide on colons", () => {
147 const store = useSidebarPendingFileDropStore.getState();
148 store.queuePendingFileDrop(makeEntry("a", "b:c", makeFiles("a.png")));
149
150 expect(
151 useSidebarPendingFileDropStore.getState().consumePendingFileDrop(refOf("a:b", "c")),
152 ).toBeNull();
153 expect(useSidebarPendingFileDropStore.getState().pending).toHaveLength(1);
154 });
155});
156
157describe("isSameSidebarThreadRef", () => {
158 it("compares fields, not joined keys", () => {
159 expect(isSameSidebarThreadRef(refOf("a", "b:c"), refOf("a", "b:c"))).toBe(true);
160 expect(isSameSidebarThreadRef(refOf("a", "b:c"), refOf("a:b", "c"))).toBe(false);
161 expect(isSameSidebarThreadRef(refOf("a", "b"), refOf("a", "c"))).toBe(false);
162 });
163});