extract_pane_to_tab_list_test.go RawHistoryFolder
1package usecase
2
3import (
4 "testing"
5
6 "github.com/bnema/dumber/internal/domain/entity"
7 "github.com/stretchr/testify/require"
8)
9
10func TestExtractPaneToTabList_FromThreePaneStack(t *testing.T) {
11 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
12 sourceTabs := entity.NewTabList()
13 targetTabs := entity.NewTabList()
14
15 paneA := entity.NewPane(entity.PaneID("pA"))
16 paneB := entity.NewPane(entity.PaneID("pB"))
17 paneC := entity.NewPane(entity.PaneID("pC"))
18 sourceTab := newStackedTab(paneA, paneB, paneC)
19 sourceTab.Workspace.ActivePaneID = paneB.ID
20 sourceTabs.Add(sourceTab)
21
22 out, err := uc.Execute(ExtractPaneToTabListInput{
23 SourceTabs: sourceTabs,
24 SourceTabID: sourceTab.ID,
25 SourcePaneID: paneB.ID,
26 TargetTabs: targetTabs,
27 })
28 require.NoError(t, err)
29 require.False(t, out.SourceTabClosed)
30 require.NotNil(t, out.NewTab)
31 require.NotNil(t, out.MovedPaneNode)
32
33 require.True(t, sourceTab.Workspace.Root.IsStacked)
34 require.Len(t, sourceTab.Workspace.Root.Children, 2)
35 require.Equal(t, paneA.ID, sourceTab.Workspace.Root.Children[0].Pane.ID)
36 require.Equal(t, paneC.ID, sourceTab.Workspace.Root.Children[1].Pane.ID)
37 require.Contains(t, []entity.PaneID{paneA.ID, paneC.ID}, sourceTab.Workspace.ActivePaneID)
38 require.Same(t, sourceTab.Workspace.Root, sourceTab.Workspace.Root.Children[0].Parent)
39 require.Same(t, sourceTab.Workspace.Root, sourceTab.Workspace.Root.Children[1].Parent)
40
41 require.Equal(t, 1, targetTabs.Count())
42 require.Same(t, out.NewTab, targetTabs.TabAt(0))
43 require.Same(t, paneB, out.NewTab.Workspace.Root.Pane)
44 require.Nil(t, out.NewTab.Workspace.Root.Parent)
45 require.Nil(t, out.MovedPaneNode.Parent)
46}
47
48func TestExtractPaneToTabList_FromThreePaneStackUsesWorkspaceActivePaneWhenStackIndexIsStale(t *testing.T) {
49 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
50 sourceTabs := entity.NewTabList()
51 targetTabs := entity.NewTabList()
52
53 paneA := entity.NewPane(entity.PaneID("pA"))
54 paneB := entity.NewPane(entity.PaneID("pB"))
55 paneC := entity.NewPane(entity.PaneID("pC"))
56 sourceTab := newStackedTab(paneA, paneB, paneC)
57 // ActivePaneID is the workspace source of truth, but stacked UI state can lag.
58 // Eject should remove pB deterministically and activate the pane that takes its slot.
59 sourceTab.Workspace.ActivePaneID = paneB.ID
60 sourceTab.Workspace.Root.ActiveStackIndex = 0
61 sourceTabs.Add(sourceTab)
62
63 out, err := uc.Execute(ExtractPaneToTabListInput{
64 SourceTabs: sourceTabs,
65 SourceTabID: sourceTab.ID,
66 SourcePaneID: paneB.ID,
67 TargetTabs: targetTabs,
68 })
69 require.NoError(t, err)
70 require.False(t, out.SourceTabClosed)
71
72 require.True(t, sourceTab.Workspace.Root.IsStacked)
73 require.Len(t, sourceTab.Workspace.Root.Children, 2)
74 require.Equal(t, paneA.ID, sourceTab.Workspace.Root.Children[0].Pane.ID)
75 require.Equal(t, paneC.ID, sourceTab.Workspace.Root.Children[1].Pane.ID)
76 require.Equal(t, 1, sourceTab.Workspace.Root.ActiveStackIndex)
77 require.Equal(t, paneC.ID, sourceTab.Workspace.ActivePaneID)
78 require.Same(t, paneB, out.NewTab.Workspace.Root.Pane)
79}
80
81func TestExtractPaneToTabList_FromTwoPaneStackDissolvesSource(t *testing.T) {
82 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
83 sourceTabs := entity.NewTabList()
84 targetTabs := entity.NewTabList()
85
86 paneA := entity.NewPane(entity.PaneID("pA"))
87 paneB := entity.NewPane(entity.PaneID("pB"))
88 sourceTab := newStackedTab(paneA, paneB)
89 sourceTab.Workspace.ActivePaneID = paneB.ID
90 sourceTabs.Add(sourceTab)
91
92 out, err := uc.Execute(ExtractPaneToTabListInput{
93 SourceTabs: sourceTabs,
94 SourceTabID: sourceTab.ID,
95 SourcePaneID: paneB.ID,
96 TargetTabs: targetTabs,
97 })
98 require.NoError(t, err)
99 require.False(t, out.SourceTabClosed)
100
101 require.NotNil(t, sourceTab.Workspace.Root)
102 require.True(t, sourceTab.Workspace.Root.IsLeaf())
103 require.Same(t, paneA, sourceTab.Workspace.Root.Pane)
104 require.Nil(t, sourceTab.Workspace.Root.Parent)
105 require.Same(t, paneB, out.NewTab.Workspace.Root.Pane)
106 require.Nil(t, out.NewTab.Workspace.Root.Parent)
107 require.Nil(t, out.MovedPaneNode.Parent)
108}
109
110func TestExtractPaneToTabList_LastPaneClosesSourceTab(t *testing.T) {
111 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
112 sourceTabs := entity.NewTabList()
113 targetTabs := entity.NewTabList()
114
115 pane := entity.NewPane(entity.PaneID("pA"))
116 sourceTab := entity.NewTab(entity.TabID("tA"), entity.WorkspaceID("wA"), pane)
117 sourceTabs.Add(sourceTab)
118
119 out, err := uc.Execute(ExtractPaneToTabListInput{
120 SourceTabs: sourceTabs,
121 SourceTabID: sourceTab.ID,
122 SourcePaneID: pane.ID,
123 TargetTabs: targetTabs,
124 })
125 require.NoError(t, err)
126 require.True(t, out.SourceTabClosed)
127 require.Nil(t, sourceTabs.Find(sourceTab.ID))
128 require.Equal(t, 0, sourceTabs.Count())
129 require.Equal(t, 1, targetTabs.Count())
130 require.Same(t, pane, out.NewTab.Workspace.Root.Pane)
131}
132
133func TestExtractPaneToTabList_FromStackPreservesActivePaneOutsideStack(t *testing.T) {
134 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
135 sourceTabs := entity.NewTabList()
136 targetTabs := entity.NewTabList()
137
138 paneA := entity.NewPane(entity.PaneID("pA"))
139 paneB := entity.NewPane(entity.PaneID("pB"))
140 paneC := entity.NewPane(entity.PaneID("pC"))
141 paneD := entity.NewPane(entity.PaneID("pD"))
142 stack := &entity.PaneNode{ID: "stack", IsStacked: true, ActiveStackIndex: 2}
143 stack.Children = []*entity.PaneNode{
144 {ID: string(paneA.ID), Pane: paneA, Parent: stack},
145 {ID: string(paneB.ID), Pane: paneB, Parent: stack},
146 {ID: string(paneC.ID), Pane: paneC, Parent: stack},
147 }
148 root := &entity.PaneNode{ID: "root", SplitDir: entity.SplitHorizontal, SplitRatio: 0.5}
149 paneDNode := &entity.PaneNode{ID: string(paneD.ID), Pane: paneD, Parent: root}
150 stack.Parent = root
151 root.Children = []*entity.PaneNode{stack, paneDNode}
152 sourceTab := &entity.Tab{ID: "tA", Workspace: &entity.Workspace{ID: "wA", Root: root, ActivePaneID: paneD.ID}}
153 sourceTabs.Add(sourceTab)
154
155 out, err := uc.Execute(ExtractPaneToTabListInput{
156 SourceTabs: sourceTabs,
157 SourceTabID: sourceTab.ID,
158 SourcePaneID: paneB.ID,
159 TargetTabs: targetTabs,
160 })
161 require.NoError(t, err)
162 require.False(t, out.SourceTabClosed)
163
164 require.Equal(t, paneD.ID, sourceTab.Workspace.ActivePaneID)
165 require.True(t, stack.IsStacked)
166 require.Len(t, stack.Children, 2)
167 require.Equal(t, 1, stack.ActiveStackIndex)
168 require.Same(t, paneB, out.NewTab.Workspace.Root.Pane)
169 assertParentPointers(t, sourceTab.Workspace.Root, nil)
170 assertParentPointers(t, out.NewTab.Workspace.Root, nil)
171}
172
173func TestExtractPaneToTabList_FromTwoPaneStackDissolvePreservesActivePaneOutsideStack(t *testing.T) {
174 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
175 sourceTabs := entity.NewTabList()
176 targetTabs := entity.NewTabList()
177
178 paneA := entity.NewPane(entity.PaneID("pA"))
179 paneB := entity.NewPane(entity.PaneID("pB"))
180 paneC := entity.NewPane(entity.PaneID("pC"))
181 stack := &entity.PaneNode{ID: "stack", IsStacked: true, ActiveStackIndex: 1}
182 stack.Children = []*entity.PaneNode{
183 {ID: string(paneA.ID), Pane: paneA, Parent: stack},
184 {ID: string(paneB.ID), Pane: paneB, Parent: stack},
185 }
186 root := &entity.PaneNode{ID: "root", SplitDir: entity.SplitHorizontal, SplitRatio: 0.5}
187 paneCNode := &entity.PaneNode{ID: string(paneC.ID), Pane: paneC, Parent: root}
188 stack.Parent = root
189 root.Children = []*entity.PaneNode{stack, paneCNode}
190 sourceTab := &entity.Tab{ID: "tA", Workspace: &entity.Workspace{ID: "wA", Root: root, ActivePaneID: paneC.ID}}
191 sourceTabs.Add(sourceTab)
192
193 out, err := uc.Execute(ExtractPaneToTabListInput{
194 SourceTabs: sourceTabs,
195 SourceTabID: sourceTab.ID,
196 SourcePaneID: paneB.ID,
197 TargetTabs: targetTabs,
198 })
199 require.NoError(t, err)
200 require.False(t, out.SourceTabClosed)
201
202 require.Equal(t, paneC.ID, sourceTab.Workspace.ActivePaneID)
203 require.True(t, stack.IsLeaf())
204 require.Same(t, paneA, stack.Pane)
205 require.Same(t, paneB, out.NewTab.Workspace.Root.Pane)
206 assertParentPointers(t, sourceTab.Workspace.Root, nil)
207 assertParentPointers(t, out.NewTab.Workspace.Root, nil)
208}
209
210func TestExtractPaneToTabList_FromNestedSplitPreservesActivePaneOutsidePromotedSibling(t *testing.T) {
211 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
212 sourceTabs := entity.NewTabList()
213 targetTabs := entity.NewTabList()
214
215 paneA := entity.NewPane(entity.PaneID("pA"))
216 paneB := entity.NewPane(entity.PaneID("pB"))
217 paneC := entity.NewPane(entity.PaneID("pC"))
218 root := &entity.PaneNode{ID: "root", SplitDir: entity.SplitHorizontal, SplitRatio: 0.5}
219 leftLeaf := &entity.PaneNode{ID: string(paneA.ID), Pane: paneA, Parent: root}
220 rightSplit := &entity.PaneNode{ID: "right-split", SplitDir: entity.SplitVertical, SplitRatio: 0.5, Parent: root}
221 paneBNode := &entity.PaneNode{ID: string(paneB.ID), Pane: paneB, Parent: rightSplit}
222 paneCNode := &entity.PaneNode{ID: string(paneC.ID), Pane: paneC, Parent: rightSplit}
223 rightSplit.Children = []*entity.PaneNode{paneBNode, paneCNode}
224 root.Children = []*entity.PaneNode{leftLeaf, rightSplit}
225 sourceTab := &entity.Tab{ID: "tA", Workspace: &entity.Workspace{ID: "wA", Root: root, ActivePaneID: paneA.ID}}
226 sourceTabs.Add(sourceTab)
227
228 out, err := uc.Execute(ExtractPaneToTabListInput{
229 SourceTabs: sourceTabs,
230 SourceTabID: sourceTab.ID,
231 SourcePaneID: paneC.ID,
232 TargetTabs: targetTabs,
233 })
234 require.NoError(t, err)
235 require.False(t, out.SourceTabClosed)
236
237 require.Equal(t, paneA.ID, sourceTab.Workspace.ActivePaneID)
238 require.Same(t, root, sourceTab.Workspace.Root)
239 require.Same(t, leftLeaf, sourceTab.Workspace.Root.Left())
240 require.Same(t, paneBNode, sourceTab.Workspace.Root.Right())
241 require.Nil(t, paneCNode.Parent)
242 require.Empty(t, rightSplit.Children)
243 require.Same(t, paneC, out.NewTab.Workspace.Root.Pane)
244 assertParentPointers(t, sourceTab.Workspace.Root, nil)
245 assertParentPointers(t, out.NewTab.Workspace.Root, nil)
246}
247
248func TestExtractPaneToTabList_FromSplitSeversMovedNode(t *testing.T) {
249 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
250 sourceTabs := entity.NewTabList()
251 targetTabs := entity.NewTabList()
252
253 paneA := entity.NewPane(entity.PaneID("pA"))
254 paneB := entity.NewPane(entity.PaneID("pB"))
255 sourceTab := entity.NewTab(entity.TabID("tA"), entity.WorkspaceID("wA"), paneA)
256 removedSplit := &entity.PaneNode{ID: "split", SplitDir: entity.SplitHorizontal, SplitRatio: 0.5}
257 movedLeaf := &entity.PaneNode{ID: "pA", Pane: paneA, Parent: removedSplit}
258 otherLeaf := &entity.PaneNode{ID: "pB", Pane: paneB, Parent: removedSplit}
259 removedSplit.Children = []*entity.PaneNode{movedLeaf, otherLeaf}
260 sourceTab.Workspace.Root = removedSplit
261 sourceTab.Workspace.ActivePaneID = paneA.ID
262 sourceTabs.Add(sourceTab)
263
264 out, err := uc.Execute(ExtractPaneToTabListInput{
265 SourceTabs: sourceTabs,
266 SourceTabID: sourceTab.ID,
267 SourcePaneID: paneA.ID,
268 TargetTabs: targetTabs,
269 })
270 require.NoError(t, err)
271 require.False(t, out.SourceTabClosed)
272
273 require.Same(t, otherLeaf, sourceTab.Workspace.Root)
274 require.Nil(t, sourceTab.Workspace.Root.Parent)
275 require.Nil(t, out.MovedPaneNode.Parent)
276 require.Empty(t, removedSplit.Children)
277 assertParentPointers(t, sourceTab.Workspace.Root, nil)
278 assertParentPointers(t, out.NewTab.Workspace.Root, nil)
279}
280
281func TestExtractPaneToTabList_InvalidSourceTabID(t *testing.T) {
282 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
283 sourceTabs := entity.NewTabList()
284 sourceTabs.Add(entity.NewTab("source-tab", "source-ws", entity.NewPane("pane-a")))
285
286 _, err := uc.Execute(ExtractPaneToTabListInput{
287 SourceTabs: sourceTabs,
288 SourceTabID: "missing-tab",
289 SourcePaneID: "pane-a",
290 TargetTabs: entity.NewTabList(),
291 })
292 require.Error(t, err)
293}
294
295func TestExtractPaneToTabList_InvalidSourcePaneID(t *testing.T) {
296 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
297 sourceTabs := entity.NewTabList()
298 sourceTab := entity.NewTab("source-tab", "source-ws", entity.NewPane("pane-a"))
299 sourceTabs.Add(sourceTab)
300
301 _, err := uc.Execute(ExtractPaneToTabListInput{
302 SourceTabs: sourceTabs,
303 SourceTabID: sourceTab.ID,
304 SourcePaneID: "missing-pane",
305 TargetTabs: entity.NewTabList(),
306 })
307 require.Error(t, err)
308}
309
310func TestExtractPaneToTabList_NilInputs(t *testing.T) {
311 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
312
313 _, err := uc.Execute(ExtractPaneToTabListInput{
314 SourceTabID: "source-tab",
315 SourcePaneID: "pane-a",
316 TargetTabs: entity.NewTabList(),
317 })
318 require.Error(t, err)
319
320 _, err = uc.Execute(ExtractPaneToTabListInput{
321 SourceTabs: entity.NewTabList(),
322 SourceTabID: "source-tab",
323 SourcePaneID: "pane-a",
324 })
325 require.Error(t, err)
326}
327
328func TestExtractPaneToTabList_ParentPointerTreeIntegrity(t *testing.T) {
329 uc := NewExtractPaneToTabListUseCase(newTestIDGen())
330 sourceTabs := entity.NewTabList()
331 targetTabs := entity.NewTabList()
332
333 paneA := entity.NewPane(entity.PaneID("pA"))
334 paneB := entity.NewPane(entity.PaneID("pB"))
335 paneC := entity.NewPane(entity.PaneID("pC"))
336 sourceTab := newStackedTab(paneA, paneB, paneC)
337 sourceTabs.Add(sourceTab)
338
339 out, err := uc.Execute(ExtractPaneToTabListInput{
340 SourceTabs: sourceTabs,
341 SourceTabID: sourceTab.ID,
342 SourcePaneID: paneB.ID,
343 TargetTabs: targetTabs,
344 })
345 require.NoError(t, err)
346
347 assertParentPointers(t, sourceTab.Workspace.Root, nil)
348 assertParentPointers(t, out.NewTab.Workspace.Root, nil)
349 require.Nil(t, out.MovedPaneNode.Parent)
350}
351
352func newStackedTab(panes ...*entity.Pane) *entity.Tab {
353 tab := entity.NewTab("tA", "wA", panes[0])
354 stack := &entity.PaneNode{ID: "stack", IsStacked: true, ActiveStackIndex: 0}
355 for _, pane := range panes {
356 child := &entity.PaneNode{ID: string(pane.ID), Pane: pane, Parent: stack}
357 stack.Children = append(stack.Children, child)
358 }
359 tab.Workspace.Root = stack
360 tab.Workspace.ActivePaneID = panes[0].ID
361 return tab
362}
363
364func assertParentPointers(t *testing.T, node, wantParent *entity.PaneNode) {
365 t.Helper()
366 if node == nil {
367 return
368 }
369 require.Same(t, wantParent, node.Parent)
370 for _, child := range node.Children {
371 assertParentPointers(t, child, node)
372 }
373}