1import { scopeThreadRef, scopedThreadKey } from "@t3tools/client-runtime/environment";
2import { ThreadId } from "@t3tools/contracts";
3import { beforeEach, describe, expect, it } from "vite-plus/test";
4
5import {
6 migratePersistedTerminalUiStateStoreState,
7 selectThreadTerminalUiState,
8 useTerminalUiStateStore,
9} from "./terminalUiStateStore";
10import { DEFAULT_THREAD_TERMINAL_ID } from "./types";
11
12const THREAD_ID = ThreadId.make("thread-1");
13const THREAD_REF = scopeThreadRef("environment-a" as never, THREAD_ID);
14const OTHER_THREAD_REF = scopeThreadRef("environment-b" as never, THREAD_ID);
15
16describe("terminalUiStateStore actions", () => {
17 beforeEach(() => {
18 useTerminalUiStateStore.persist.clearStorage();
19 useTerminalUiStateStore.setState({
20 terminalUiStateByThreadKey: {},
21 suppressedTerminalIdsByThreadKey: {},
22 });
23 });
24
25 it("caps splits at four terminals per group", () => {
26 const store = useTerminalUiStateStore.getState();
27 store.splitTerminal(THREAD_REF, "terminal-2");
28 store.splitTerminal(THREAD_REF, "terminal-3");
29 store.splitTerminal(THREAD_REF, "terminal-4");
30 store.splitTerminal(THREAD_REF, "terminal-5");
31 store.splitTerminal(THREAD_REF, "terminal-6");
32
33 const terminalUiState = selectThreadTerminalUiState(
34 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
35 THREAD_REF,
36 );
37 expect(terminalUiState.terminalIds).toEqual([
38 "terminal-2",
39 "terminal-3",
40 "terminal-4",
41 "terminal-5",
42 ]);
43 expect(terminalUiState.terminalGroups).toEqual([
44 {
45 id: "group-terminal-2",
46 terminalIds: ["terminal-2", "terminal-3", "terminal-4", "terminal-5"],
47 },
48 ]);
49 });
50
51 it("keeps state isolated per environment when raw thread ids collide", () => {
52 const store = useTerminalUiStateStore.getState();
53 store.setTerminalOpen(THREAD_REF, true);
54 store.newTerminal(OTHER_THREAD_REF, "env-b-terminal");
55
56 expect(
57 selectThreadTerminalUiState(
58 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
59 THREAD_REF,
60 ).terminalOpen,
61 ).toBe(true);
62 expect(
63 selectThreadTerminalUiState(
64 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
65 OTHER_THREAD_REF,
66 ).terminalIds,
67 ).toEqual(["env-b-terminal"]);
68 });
69
70 it("drops persisted entries whose thread keys are not valid scoped keys", () => {
71 const migrated = migratePersistedTerminalUiStateStoreState(
72 {
73 terminalStateByThreadKey: {
74 [scopedThreadKey(THREAD_REF)]: {
75 terminalOpen: true,
76 terminalHeight: 320,
77 terminalIds: ["term-1"],
78 activeTerminalId: "term-1",
79 terminalGroups: [{ id: "group-term-1", terminalIds: ["term-1"] }],
80 activeTerminalGroupId: "group-term-1",
81 },
82 "legacy-thread-id": {
83 terminalOpen: true,
84 terminalHeight: 320,
85 terminalIds: ["term-1"],
86 activeTerminalId: "term-1",
87 terminalGroups: [{ id: "group-term-1", terminalIds: ["term-1"] }],
88 activeTerminalGroupId: "group-term-1",
89 },
90 },
91 },
92 2,
93 );
94
95 expect(migrated).toEqual({
96 terminalUiStateByThreadKey: {
97 [scopedThreadKey(THREAD_REF)]: {
98 terminalOpen: true,
99 terminalHeight: 320,
100 terminalIds: ["term-1"],
101 activeTerminalId: "term-1",
102 terminalGroups: [{ id: "group-term-1", terminalIds: ["term-1"] }],
103 activeTerminalGroupId: "group-term-1",
104 },
105 },
106 });
107 });
108
109 it("resets to default and clears persisted entry when closing the last terminal", () => {
110 const store = useTerminalUiStateStore.getState();
111 store.newTerminal(THREAD_REF, "terminal-only");
112 store.closeTerminal(THREAD_REF, "terminal-only");
113
114 expect(
115 useTerminalUiStateStore.getState().terminalUiStateByThreadKey[scopedThreadKey(THREAD_REF)],
116 ).toBeUndefined();
117 expect(
118 selectThreadTerminalUiState(
119 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
120 THREAD_REF,
121 ).terminalIds,
122 ).toEqual([]);
123 });
124
125 it("keeps a valid active terminal after closing an active split terminal", () => {
126 const store = useTerminalUiStateStore.getState();
127 store.splitTerminal(THREAD_REF, "terminal-2");
128 store.splitTerminal(THREAD_REF, "terminal-3");
129 store.closeTerminal(THREAD_REF, "terminal-3");
130
131 const terminalUiState = selectThreadTerminalUiState(
132 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
133 THREAD_REF,
134 );
135 expect(terminalUiState.activeTerminalId).toBe("terminal-2");
136 expect(terminalUiState.terminalIds).toEqual(["terminal-2"]);
137 expect(terminalUiState.terminalGroups).toEqual([
138 { id: "group-terminal-2", terminalIds: ["terminal-2"] },
139 ]);
140 });
141
142 it.each([
143 ["terminal-3", "terminal-3", DEFAULT_THREAD_TERMINAL_ID],
144 [DEFAULT_THREAD_TERMINAL_ID, DEFAULT_THREAD_TERMINAL_ID, "terminal-3"],
145 ["terminal-3", DEFAULT_THREAD_TERMINAL_ID, "terminal-3"],
146 ])("keeps the split's tab when %s is active and %s closes", (activeId, closedId, nextId) => {
147 const store = useTerminalUiStateStore.getState();
148 store.setTerminalOpen(THREAD_REF, true);
149 store.newTerminal(THREAD_REF, "terminal-2");
150 store.setActiveTerminal(THREAD_REF, DEFAULT_THREAD_TERMINAL_ID);
151 store.splitTerminal(THREAD_REF, "terminal-3");
152 store.setActiveTerminal(THREAD_REF, activeId);
153 store.closeTerminal(THREAD_REF, closedId);
154
155 const terminalUiState = selectThreadTerminalUiState(
156 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
157 THREAD_REF,
158 );
159 expect(terminalUiState.activeTerminalId).toBe(nextId);
160 expect(terminalUiState.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
161 });
162
163 it("does not import a closed panel terminal from stale metadata", () => {
164 const store = useTerminalUiStateStore.getState();
165 store.newTerminal(THREAD_REF, "term-2");
166 store.closeTerminal(THREAD_REF, "term-1");
167
168 store.reconcileTerminalIds(THREAD_REF, ["term-1", "term-2"]);
169
170 expect(
171 selectThreadTerminalUiState(
172 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
173 THREAD_REF,
174 ).terminalIds,
175 ).toEqual(["term-2"]);
176
177 store.newTerminal(THREAD_REF, "term-1");
178 expect(
179 selectThreadTerminalUiState(
180 useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
181 THREAD_REF,
182 ).terminalIds,
183 ).toEqual(["term-2", "term-1"]);
184 });
185});