Skip to content
File

Blob: test/store.test.ts

typescript233 lines
1// @vitest-environment jsdom
2 
3/**
4 * Unit tests for the workspace zustand store.
5 *
6 * Covers:
7 * - tab CRUD (new, close, switch)
8 * - session replacement (new session, reconnect)
9 * - connection state tracking
10 * - default initialization
11 * - close-tab edge cases (last tab, active tab neighbor selection)
12 */
13import { describe, it, expect, beforeEach } from "vitest";
14import { useWorkspaceStore } from "../src/client/workspace/store";
15 
16// ---------------------------------------------------------------------------
17// Helpers
18// ---------------------------------------------------------------------------
19 
20function resetStore() {
21 useWorkspaceStore.setState(useWorkspaceStore.getInitialState());
22}
23 
24function getState() {
25 return useWorkspaceStore.getState();
26}
27 
28// ---------------------------------------------------------------------------
29// Tests
30// ---------------------------------------------------------------------------
31 
32beforeEach(() => {
33 resetStore();
34});
35 
36describe("WorkspaceStore — initialization", () => {
37 it("starts with one default tab", () => {
38 const { tabs, activeTabId, nextTabNumber } = getState();
39 expect(tabs).toHaveLength(1);
40 expect(tabs[0].id).toBe(activeTabId);
41 expect(tabs[0].title).toBe("Terminal 1");
42 expect(tabs[0].sessionId).toMatch(/^s-/);
43 expect(tabs[0].id).toMatch(/^t-/);
44 expect(nextTabNumber).toBe(2);
45 });
46 
47 it("starts with empty ephemeral state", () => {
48 const { reconnectKeys, connectionStates, isSharedSession } = getState();
49 expect(reconnectKeys).toEqual({});
50 expect(connectionStates).toEqual({});
51 expect(isSharedSession).toBe(false);
52 });
53});
54 
55describe("WorkspaceStore — newTab", () => {
56 it("creates a new tab and activates it", () => {
57 const originalTab = getState().tabs[0];
58 getState().newTab();
59 
60 const { tabs, activeTabId, nextTabNumber } = getState();
61 expect(tabs).toHaveLength(2);
62 expect(tabs[0]).toBe(originalTab);
63 expect(tabs[1].title).toBe("Terminal 2");
64 expect(tabs[1].id).toBe(activeTabId);
65 expect(nextTabNumber).toBe(3);
66 });
67 
68 it("increments tab number monotonically", () => {
69 getState().newTab();
70 getState().newTab();
71 const { tabs, nextTabNumber } = getState();
72 expect(tabs).toHaveLength(3);
73 expect(tabs[2].title).toBe("Terminal 3");
74 expect(nextTabNumber).toBe(4);
75 });
76 
77 it("assigns unique session IDs", () => {
78 getState().newTab();
79 const { tabs } = getState();
80 expect(tabs[0].sessionId).not.toBe(tabs[1].sessionId);
81 });
82});
83 
84describe("WorkspaceStore — closeTab", () => {
85 it("removes a tab", () => {
86 getState().newTab();
87 const { tabs } = getState();
88 expect(tabs).toHaveLength(2);
89 
90 getState().closeTab(tabs[0].id);
91 expect(getState().tabs).toHaveLength(1);
92 expect(getState().tabs[0].id).toBe(tabs[1].id);
93 });
94 
95 it("activates left neighbor when closing active tab", () => {
96 getState().newTab();
97 getState().newTab();
98 // tabs: [T1, T2, T3], active = T3
99 const tabs = getState().tabs;
100 const t3 = tabs[2];
101 const t2 = tabs[1];
102 
103 getState().closeTab(t3.id);
104 expect(getState().activeTabId).toBe(t2.id);
105 });
106 
107 it("activates right neighbor when closing first active tab", () => {
108 getState().newTab();
109 // tabs: [T1, T2], active = T2
110 const tabs = getState().tabs;
111 const t1 = tabs[0];
112 const t2 = tabs[1];
113 
114 // Switch to T1, then close it
115 getState().switchTab(t1.id);
116 getState().closeTab(t1.id);
117 expect(getState().activeTabId).toBe(t2.id);
118 });
119 
120 it("creates a new default tab when closing the last tab", () => {
121 const original = getState().tabs[0];
122 getState().closeTab(original.id);
123 
124 const { tabs, activeTabId } = getState();
125 expect(tabs).toHaveLength(1);
126 expect(tabs[0].id).not.toBe(original.id);
127 expect(tabs[0].id).toBe(activeTabId);
128 });
129 
130 it("cleans up ephemeral state for closed tab", () => {
131 getState().newTab();
132 const tabs = getState().tabs;
133 const tabToClose = tabs[0];
134 
135 // Set some ephemeral state
136 getState().setConnectionState(tabToClose.id, "connected");
137 useWorkspaceStore.setState({
138 reconnectKeys: { [tabToClose.id]: 3 },
139 });
140 
141 getState().closeTab(tabToClose.id);
142 expect(getState().connectionStates[tabToClose.id]).toBeUndefined();
143 expect(getState().reconnectKeys[tabToClose.id]).toBeUndefined();
144 });
145});
146 
147describe("WorkspaceStore — switchTab", () => {
148 it("changes active tab without creating sessions", () => {
149 getState().newTab();
150 const tabs = getState().tabs;
151 const t1 = tabs[0];
152 
153 getState().switchTab(t1.id);
154 expect(getState().activeTabId).toBe(t1.id);
155 // Still only 2 tabs
156 expect(getState().tabs).toHaveLength(2);
157 });
158});
159 
160describe("WorkspaceStore — reconnect", () => {
161 it("increments reconnect key for active tab", () => {
162 const { activeTabId } = getState();
163 expect(getState().reconnectKeys[activeTabId]).toBeUndefined();
164 
165 getState().reconnect();
166 expect(getState().reconnectKeys[activeTabId]).toBe(1);
167 
168 getState().reconnect();
169 expect(getState().reconnectKeys[activeTabId]).toBe(2);
170 });
171 
172 it("does not affect other tabs", () => {
173 getState().newTab();
174 const tabs = getState().tabs;
175 const t1 = tabs[0];
176 // Active is T2
177 
178 getState().reconnect();
179 expect(getState().reconnectKeys[t1.id]).toBeUndefined();
180 });
181});
182 
183describe("WorkspaceStore — newSession", () => {
184 it("replaces active tab's session ID", () => {
185 const oldSessionId = getState().tabs[0].sessionId;
186 getState().newSession();
187 
188 const { tabs } = getState();
189 expect(tabs[0].sessionId).not.toBe(oldSessionId);
190 expect(tabs[0].sessionId).toMatch(/^s-/);
191 });
192 
193 it("increments reconnect key", () => {
194 const { activeTabId } = getState();
195 getState().newSession();
196 expect(getState().reconnectKeys[activeTabId]).toBe(1);
197 });
198 
199 it("does not create a new tab", () => {
200 getState().newSession();
201 expect(getState().tabs).toHaveLength(1);
202 });
203});
204 
205describe("WorkspaceStore — setConnectionState", () => {
206 it("updates per-tab connection state", () => {
207 const { activeTabId } = getState();
208 getState().setConnectionState(activeTabId, "connected");
209 expect(getState().connectionStates[activeTabId]).toBe("connected");
210 });
211 
212 it("tracks multiple tabs independently", () => {
213 getState().newTab();
214 const tabs = getState().tabs;
215 
216 getState().setConnectionState(tabs[0].id, "connected");
217 getState().setConnectionState(tabs[1].id, "connecting");
218 
219 expect(getState().connectionStates[tabs[0].id]).toBe("connected");
220 expect(getState().connectionStates[tabs[1].id]).toBe("connecting");
221 });
222});
223 
224describe("WorkspaceStore — setSharedSession", () => {
225 it("updates shared session flag", () => {
226 expect(getState().isSharedSession).toBe(false);
227 getState().setSharedSession(true);
228 expect(getState().isSharedSession).toBe(true);
229 getState().setSharedSession(false);
230 expect(getState().isSharedSession).toBe(false);
231 });
232});