main 5041353

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close