detached 29bc50f

test(manage-panes): add test coverage for closeStackedPane

commit
29bc50f98007639f16a0c5de96aa5c2d4c2881fe
author
brice · · 2026-01-08 13:15 UTC
parent
0a598e1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +251 -0
A internal/application/usecase/manage_panes_close_stack_test.go +251 -0
@@ -0,0 +1,251 @@
1+package usecase
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+func TestManagePanesUseCase_CloseStackedPane_RemovesFromStackWithMoreThanTwo(t *testing.T) {
11+ uc := NewManagePanesUseCase(func() string { return "id" })
12+ ctx := context.Background()
13+
14+ // Create a stack with 3 panes
15+ a := leaf("a")
16+ b := leaf("b")
17+ c := leaf("c")
18+ stackNode := stack(a, b, c)
19+ stackNode.ActiveStackIndex = 1 // b is active
20+
21+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: b.Pane.ID}
22+
23+ // Close pane b (the middle one)
24+ result, err := uc.Close(ctx, ws, b)
25+ if err != nil {
26+ t.Fatalf("unexpected error: %v", err)
27+ }
28+
29+ // Stack should remain with 2 panes
30+ if !ws.Root.IsStacked {
31+ t.Fatalf("root should still be a stack")
32+ }
33+ if len(ws.Root.Children) != 2 {
34+ t.Fatalf("stack should have 2 children, got %d", len(ws.Root.Children))
35+ }
36+ if got := panesInOrder(ws.Root); got != "a,c" {
37+ t.Fatalf("remaining panes=%s, want a,c", got)
38+ }
39+
40+ // Result should be the stack node
41+ if result != stackNode {
42+ t.Fatalf("expected stack node to be returned")
43+ }
44+}
45+
46+func TestManagePanesUseCase_CloseStackedPane_DissolvesStackWithTwo(t *testing.T) {
47+ uc := NewManagePanesUseCase(func() string { return "id" })
48+ ctx := context.Background()
49+
50+ // Create a stack with 2 panes
51+ a := leaf("a")
52+ b := leaf("b")
53+ stackNode := stack(a, b)
54+ stackNode.ActiveStackIndex = 1 // b is active
55+
56+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: b.Pane.ID}
57+
58+ // Close pane b
59+ result, err := uc.Close(ctx, ws, b)
60+ if err != nil {
61+ t.Fatalf("unexpected error: %v", err)
62+ }
63+
64+ // Stack should be dissolved, remaining pane becomes root
65+ if ws.Root.IsStacked {
66+ t.Fatalf("root should no longer be a stack")
67+ }
68+ if !ws.Root.IsLeaf() {
69+ t.Fatalf("root should be a leaf")
70+ }
71+ if ws.Root.Pane.ID != "a" {
72+ t.Fatalf("remaining pane should be a, got %s", ws.Root.Pane.ID)
73+ }
74+
75+ // Result should be the remaining pane
76+ if result.Pane.ID != "a" {
77+ t.Fatalf("expected remaining pane a to be returned")
78+ }
79+
80+ // Active pane should update to remaining pane
81+ if ws.ActivePaneID != "a" {
82+ t.Fatalf("active pane should be a, got %s", ws.ActivePaneID)
83+ }
84+}
85+
86+func TestManagePanesUseCase_CloseStackedPane_UpdatesActivePaneWhenClosingActive(t *testing.T) {
87+ uc := NewManagePanesUseCase(func() string { return "id" })
88+ ctx := context.Background()
89+
90+ // Create a stack with 3 panes, middle one is active
91+ a := leaf("a")
92+ b := leaf("b")
93+ c := leaf("c")
94+ stackNode := stack(a, b, c)
95+ stackNode.ActiveStackIndex = 1 // b is active
96+
97+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: b.Pane.ID}
98+
99+ // Close pane b (the active one)
100+ _, err := uc.Close(ctx, ws, b)
101+ if err != nil {
102+ t.Fatalf("unexpected error: %v", err)
103+ }
104+
105+ // Active pane should be updated (to the pane at the new active index)
106+ if ws.ActivePaneID == "b" {
107+ t.Fatalf("active pane should not be the closed pane b")
108+ }
109+ // Should be either a or c depending on new ActiveStackIndex
110+ if ws.ActivePaneID != "a" && ws.ActivePaneID != "c" {
111+ t.Fatalf("active pane should be a or c, got %s", ws.ActivePaneID)
112+ }
113+}
114+
115+func TestManagePanesUseCase_CloseStackedPane_PreservesActivePaneWhenClosingNonActive(t *testing.T) {
116+ uc := NewManagePanesUseCase(func() string { return "id" })
117+ ctx := context.Background()
118+
119+ // Create a stack with 3 panes, first one is active
120+ a := leaf("a")
121+ b := leaf("b")
122+ c := leaf("c")
123+ stackNode := stack(a, b, c)
124+ stackNode.ActiveStackIndex = 0 // a is active
125+
126+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: a.Pane.ID}
127+
128+ // Close pane c (not the active one)
129+ _, err := uc.Close(ctx, ws, c)
130+ if err != nil {
131+ t.Fatalf("unexpected error: %v", err)
132+ }
133+
134+ // Active pane should remain a
135+ if ws.ActivePaneID != "a" {
136+ t.Fatalf("active pane should still be a, got %s", ws.ActivePaneID)
137+ }
138+}
139+
140+func TestManagePanesUseCase_CloseStackedPane_DissolvesNestedStack(t *testing.T) {
141+ uc := NewManagePanesUseCase(func() string { return "id" })
142+ ctx := context.Background()
143+
144+ // Create a split with a stack on the left
145+ a := leaf("a")
146+ b := leaf("b")
147+ stackNode := stack(a, b)
148+ stackNode.ActiveStackIndex = 0
149+
150+ c := leaf("c")
151+ root := split(entity.SplitHorizontal, stackNode, c)
152+
153+ ws := &entity.Workspace{Root: root, ActivePaneID: a.Pane.ID}
154+
155+ // Close pane a from the stack
156+ _, err := uc.Close(ctx, ws, a)
157+ if err != nil {
158+ t.Fatalf("unexpected error: %v", err)
159+ }
160+
161+ // Root should still be a split
162+ if !ws.Root.IsSplit() {
163+ t.Fatalf("root should still be a split")
164+ }
165+
166+ // Left child should now be the remaining pane b (not a stack)
167+ left := ws.Root.Left()
168+ if left.IsStacked {
169+ t.Fatalf("left child should no longer be a stack")
170+ }
171+ if !left.IsLeaf() || left.Pane.ID != "b" {
172+ t.Fatalf("left child should be leaf b, got %v", left)
173+ }
174+
175+ // Right child should still be c
176+ right := ws.Root.Right()
177+ if !right.IsLeaf() || right.Pane.ID != "c" {
178+ t.Fatalf("right child should be leaf c")
179+ }
180+
181+ // Active pane should update to b
182+ if ws.ActivePaneID != "b" {
183+ t.Fatalf("active pane should be b, got %s", ws.ActivePaneID)
184+ }
185+}
186+
187+func TestManagePanesUseCase_CloseStackedPane_ClosingFirstPaneUpdatesIndex(t *testing.T) {
188+ uc := NewManagePanesUseCase(func() string { return "id" })
189+ ctx := context.Background()
190+
191+ // Create a stack with 3 panes, first one is active
192+ a := leaf("a")
193+ b := leaf("b")
194+ c := leaf("c")
195+ stackNode := stack(a, b, c)
196+ stackNode.ActiveStackIndex = 0 // a is active
197+
198+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: a.Pane.ID}
199+
200+ // Close pane a (the first and active one)
201+ _, err := uc.Close(ctx, ws, a)
202+ if err != nil {
203+ t.Fatalf("unexpected error: %v", err)
204+ }
205+
206+ // Stack should have 2 panes remaining
207+ if len(ws.Root.Children) != 2 {
208+ t.Fatalf("stack should have 2 children, got %d", len(ws.Root.Children))
209+ }
210+ if got := panesInOrder(ws.Root); got != "b,c" {
211+ t.Fatalf("remaining panes=%s, want b,c", got)
212+ }
213+
214+ // Active pane should be updated
215+ if ws.ActivePaneID == "a" {
216+ t.Fatalf("active pane should not be the closed pane a")
217+ }
218+}
219+
220+func TestManagePanesUseCase_CloseStackedPane_ClosingLastPaneUpdatesIndex(t *testing.T) {
221+ uc := NewManagePanesUseCase(func() string { return "id" })
222+ ctx := context.Background()
223+
224+ // Create a stack with 3 panes, last one is active
225+ a := leaf("a")
226+ b := leaf("b")
227+ c := leaf("c")
228+ stackNode := stack(a, b, c)
229+ stackNode.ActiveStackIndex = 2 // c is active
230+
231+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: c.Pane.ID}
232+
233+ // Close pane c (the last and active one)
234+ _, err := uc.Close(ctx, ws, c)
235+ if err != nil {
236+ t.Fatalf("unexpected error: %v", err)
237+ }
238+
239+ // Stack should have 2 panes remaining
240+ if len(ws.Root.Children) != 2 {
241+ t.Fatalf("stack should have 2 children, got %d", len(ws.Root.Children))
242+ }
243+ if got := panesInOrder(ws.Root); got != "a,b" {
244+ t.Fatalf("remaining panes=%s, want a,b", got)
245+ }
246+
247+ // Active pane should be updated to the new last pane
248+ if ws.ActivePaneID == "c" {
249+ t.Fatalf("active pane should not be the closed pane c")
250+ }
251+}
@@ -0,0 +1,251 @@
1+package usecase
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+func TestManagePanesUseCase_CloseStackedPane_RemovesFromStackWithMoreThanTwo(t *testing.T) {
11+ uc := NewManagePanesUseCase(func() string { return "id" })
12+ ctx := context.Background()
13+
14+ // Create a stack with 3 panes
15+ a := leaf("a")
16+ b := leaf("b")
17+ c := leaf("c")
18+ stackNode := stack(a, b, c)
19+ stackNode.ActiveStackIndex = 1 // b is active
20+
21+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: b.Pane.ID}
22+
23+ // Close pane b (the middle one)
24+ result, err := uc.Close(ctx, ws, b)
25+ if err != nil {
26+ t.Fatalf("unexpected error: %v", err)
27+ }
28+
29+ // Stack should remain with 2 panes
30+ if !ws.Root.IsStacked {
31+ t.Fatalf("root should still be a stack")
32+ }
33+ if len(ws.Root.Children) != 2 {
34+ t.Fatalf("stack should have 2 children, got %d", len(ws.Root.Children))
35+ }
36+ if got := panesInOrder(ws.Root); got != "a,c" {
37+ t.Fatalf("remaining panes=%s, want a,c", got)
38+ }
39+
40+ // Result should be the stack node
41+ if result != stackNode {
42+ t.Fatalf("expected stack node to be returned")
43+ }
44+}
45+
46+func TestManagePanesUseCase_CloseStackedPane_DissolvesStackWithTwo(t *testing.T) {
47+ uc := NewManagePanesUseCase(func() string { return "id" })
48+ ctx := context.Background()
49+
50+ // Create a stack with 2 panes
51+ a := leaf("a")
52+ b := leaf("b")
53+ stackNode := stack(a, b)
54+ stackNode.ActiveStackIndex = 1 // b is active
55+
56+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: b.Pane.ID}
57+
58+ // Close pane b
59+ result, err := uc.Close(ctx, ws, b)
60+ if err != nil {
61+ t.Fatalf("unexpected error: %v", err)
62+ }
63+
64+ // Stack should be dissolved, remaining pane becomes root
65+ if ws.Root.IsStacked {
66+ t.Fatalf("root should no longer be a stack")
67+ }
68+ if !ws.Root.IsLeaf() {
69+ t.Fatalf("root should be a leaf")
70+ }
71+ if ws.Root.Pane.ID != "a" {
72+ t.Fatalf("remaining pane should be a, got %s", ws.Root.Pane.ID)
73+ }
74+
75+ // Result should be the remaining pane
76+ if result.Pane.ID != "a" {
77+ t.Fatalf("expected remaining pane a to be returned")
78+ }
79+
80+ // Active pane should update to remaining pane
81+ if ws.ActivePaneID != "a" {
82+ t.Fatalf("active pane should be a, got %s", ws.ActivePaneID)
83+ }
84+}
85+
86+func TestManagePanesUseCase_CloseStackedPane_UpdatesActivePaneWhenClosingActive(t *testing.T) {
87+ uc := NewManagePanesUseCase(func() string { return "id" })
88+ ctx := context.Background()
89+
90+ // Create a stack with 3 panes, middle one is active
91+ a := leaf("a")
92+ b := leaf("b")
93+ c := leaf("c")
94+ stackNode := stack(a, b, c)
95+ stackNode.ActiveStackIndex = 1 // b is active
96+
97+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: b.Pane.ID}
98+
99+ // Close pane b (the active one)
100+ _, err := uc.Close(ctx, ws, b)
101+ if err != nil {
102+ t.Fatalf("unexpected error: %v", err)
103+ }
104+
105+ // Active pane should be updated (to the pane at the new active index)
106+ if ws.ActivePaneID == "b" {
107+ t.Fatalf("active pane should not be the closed pane b")
108+ }
109+ // Should be either a or c depending on new ActiveStackIndex
110+ if ws.ActivePaneID != "a" && ws.ActivePaneID != "c" {
111+ t.Fatalf("active pane should be a or c, got %s", ws.ActivePaneID)
112+ }
113+}
114+
115+func TestManagePanesUseCase_CloseStackedPane_PreservesActivePaneWhenClosingNonActive(t *testing.T) {
116+ uc := NewManagePanesUseCase(func() string { return "id" })
117+ ctx := context.Background()
118+
119+ // Create a stack with 3 panes, first one is active
120+ a := leaf("a")
121+ b := leaf("b")
122+ c := leaf("c")
123+ stackNode := stack(a, b, c)
124+ stackNode.ActiveStackIndex = 0 // a is active
125+
126+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: a.Pane.ID}
127+
128+ // Close pane c (not the active one)
129+ _, err := uc.Close(ctx, ws, c)
130+ if err != nil {
131+ t.Fatalf("unexpected error: %v", err)
132+ }
133+
134+ // Active pane should remain a
135+ if ws.ActivePaneID != "a" {
136+ t.Fatalf("active pane should still be a, got %s", ws.ActivePaneID)
137+ }
138+}
139+
140+func TestManagePanesUseCase_CloseStackedPane_DissolvesNestedStack(t *testing.T) {
141+ uc := NewManagePanesUseCase(func() string { return "id" })
142+ ctx := context.Background()
143+
144+ // Create a split with a stack on the left
145+ a := leaf("a")
146+ b := leaf("b")
147+ stackNode := stack(a, b)
148+ stackNode.ActiveStackIndex = 0
149+
150+ c := leaf("c")
151+ root := split(entity.SplitHorizontal, stackNode, c)
152+
153+ ws := &entity.Workspace{Root: root, ActivePaneID: a.Pane.ID}
154+
155+ // Close pane a from the stack
156+ _, err := uc.Close(ctx, ws, a)
157+ if err != nil {
158+ t.Fatalf("unexpected error: %v", err)
159+ }
160+
161+ // Root should still be a split
162+ if !ws.Root.IsSplit() {
163+ t.Fatalf("root should still be a split")
164+ }
165+
166+ // Left child should now be the remaining pane b (not a stack)
167+ left := ws.Root.Left()
168+ if left.IsStacked {
169+ t.Fatalf("left child should no longer be a stack")
170+ }
171+ if !left.IsLeaf() || left.Pane.ID != "b" {
172+ t.Fatalf("left child should be leaf b, got %v", left)
173+ }
174+
175+ // Right child should still be c
176+ right := ws.Root.Right()
177+ if !right.IsLeaf() || right.Pane.ID != "c" {
178+ t.Fatalf("right child should be leaf c")
179+ }
180+
181+ // Active pane should update to b
182+ if ws.ActivePaneID != "b" {
183+ t.Fatalf("active pane should be b, got %s", ws.ActivePaneID)
184+ }
185+}
186+
187+func TestManagePanesUseCase_CloseStackedPane_ClosingFirstPaneUpdatesIndex(t *testing.T) {
188+ uc := NewManagePanesUseCase(func() string { return "id" })
189+ ctx := context.Background()
190+
191+ // Create a stack with 3 panes, first one is active
192+ a := leaf("a")
193+ b := leaf("b")
194+ c := leaf("c")
195+ stackNode := stack(a, b, c)
196+ stackNode.ActiveStackIndex = 0 // a is active
197+
198+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: a.Pane.ID}
199+
200+ // Close pane a (the first and active one)
201+ _, err := uc.Close(ctx, ws, a)
202+ if err != nil {
203+ t.Fatalf("unexpected error: %v", err)
204+ }
205+
206+ // Stack should have 2 panes remaining
207+ if len(ws.Root.Children) != 2 {
208+ t.Fatalf("stack should have 2 children, got %d", len(ws.Root.Children))
209+ }
210+ if got := panesInOrder(ws.Root); got != "b,c" {
211+ t.Fatalf("remaining panes=%s, want b,c", got)
212+ }
213+
214+ // Active pane should be updated
215+ if ws.ActivePaneID == "a" {
216+ t.Fatalf("active pane should not be the closed pane a")
217+ }
218+}
219+
220+func TestManagePanesUseCase_CloseStackedPane_ClosingLastPaneUpdatesIndex(t *testing.T) {
221+ uc := NewManagePanesUseCase(func() string { return "id" })
222+ ctx := context.Background()
223+
224+ // Create a stack with 3 panes, last one is active
225+ a := leaf("a")
226+ b := leaf("b")
227+ c := leaf("c")
228+ stackNode := stack(a, b, c)
229+ stackNode.ActiveStackIndex = 2 // c is active
230+
231+ ws := &entity.Workspace{Root: stackNode, ActivePaneID: c.Pane.ID}
232+
233+ // Close pane c (the last and active one)
234+ _, err := uc.Close(ctx, ws, c)
235+ if err != nil {
236+ t.Fatalf("unexpected error: %v", err)
237+ }
238+
239+ // Stack should have 2 panes remaining
240+ if len(ws.Root.Children) != 2 {
241+ t.Fatalf("stack should have 2 children, got %d", len(ws.Root.Children))
242+ }
243+ if got := panesInOrder(ws.Root); got != "a,b" {
244+ t.Fatalf("remaining panes=%s, want a,b", got)
245+ }
246+
247+ // Active pane should be updated to the new last pane
248+ if ws.ActivePaneID == "c" {
249+ t.Fatalf("active pane should not be the closed pane c")
250+ }
251+}

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