detached dc9d28d

test(resize): cover split resizing and edge cases

commit
dc9d28d974549919687b3c349a018697c774a58e
author
brice · · 2025-12-26 07:13 UTC
parent
9246f54
signature
unsigned
3 files · +172 -9
M internal/application/usecase/manage_panes.go +3 -3
@@ -331,10 +331,10 @@ func axisForResizeDirection(dir ResizeDirection) (resizeAxis, bool) {
331331 }
332332
333333 func deltaForDividerMove(dir ResizeDirection, stepPercent float64) float64 {
334- delta := stepPercent / 100.0
335- if delta < 0 {
336- delta = -delta
334+ if stepPercent < 0 {
335+ stepPercent = -stepPercent
337336 }
337+ delta := stepPercent / 100.0
338338
339339 switch dir {
340340 case ResizeIncreaseRight, ResizeIncreaseDown:
@@ -331,10 +331,10 @@ func axisForResizeDirection(dir ResizeDirection) (resizeAxis, bool) {
331 }331 }
332 332
333 func deltaForDividerMove(dir ResizeDirection, stepPercent float64) float64 {333 func deltaForDividerMove(dir ResizeDirection, stepPercent float64) float64 {
334- delta := stepPercent / 100.0334+ if stepPercent < 0 {
335- if delta < 0 {335+ stepPercent = -stepPercent
336- delta = -delta
337 }336 }
337+ delta := stepPercent / 100.0
338 338
339 switch dir {339 switch dir {
340 case ResizeIncreaseRight, ResizeIncreaseDown:340 case ResizeIncreaseRight, ResizeIncreaseDown:
A internal/application/usecase/manage_panes_resize_test.go +163 -0
@@ -0,0 +1,163 @@
1+package usecase
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/domain/entity"
9+)
10+
11+func TestManagePanesUseCase_Resize_Errors(t *testing.T) {
12+ uc := NewManagePanesUseCase(func() string { return "id" })
13+
14+ ctx := context.Background()
15+
16+ err := uc.Resize(ctx, nil, nil, ResizeIncreaseDown, 5, 10)
17+ if err == nil {
18+ t.Fatalf("expected error when workspace is nil")
19+ }
20+
21+ ws := &entity.Workspace{}
22+ err = uc.Resize(ctx, ws, nil, ResizeIncreaseDown, 5, 10)
23+ if err == nil {
24+ t.Fatalf("expected error when pane node is nil")
25+ }
26+
27+ // Root nil should return ErrNothingToResize.
28+ leaf := &entity.PaneNode{ID: "p1", Pane: &entity.Pane{ID: "p1"}}
29+ err = uc.Resize(ctx, ws, leaf, ResizeIncreaseDown, 5, 10)
30+ if !errors.Is(err, ErrNothingToResize) {
31+ t.Fatalf("expected ErrNothingToResize, got %v", err)
32+ }
33+}
34+
35+func TestManagePanesUseCase_Resize_VerticalDividerMove(t *testing.T) {
36+ uc := NewManagePanesUseCase(func() string { return "id" })
37+ ctx := context.Background()
38+
39+ top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}
40+ bottom := &entity.PaneNode{ID: "bottom", Pane: &entity.Pane{ID: "bottom"}}
41+ root := &entity.PaneNode{
42+ ID: "split",
43+ SplitDir: entity.SplitVertical,
44+ SplitRatio: 0.5,
45+ Children: []*entity.PaneNode{top, bottom},
46+ }
47+ top.Parent = root
48+ bottom.Parent = root
49+
50+ ws := &entity.Workspace{Root: root, ActivePaneID: "bottom"}
51+
52+ // Moving divider down increases the first child's ratio.
53+ if err := uc.Resize(ctx, ws, bottom, ResizeIncreaseDown, 5.0, 10.0); err != nil {
54+ t.Fatalf("unexpected error: %v", err)
55+ }
56+ if got, want := root.SplitRatio, 0.55; got != want {
57+ t.Fatalf("split ratio = %v, want %v", got, want)
58+ }
59+
60+ // Moving divider up decreases the first child's ratio.
61+ if err := uc.Resize(ctx, ws, bottom, ResizeIncreaseUp, 5.0, 10.0); err != nil {
62+ t.Fatalf("unexpected error: %v", err)
63+ }
64+ if got, want := root.SplitRatio, 0.5; got != want {
65+ t.Fatalf("split ratio = %v, want %v", got, want)
66+ }
67+}
68+
69+func TestManagePanesUseCase_Resize_SmartResizeGrowsAndShrinksActivePane(t *testing.T) {
70+ uc := NewManagePanesUseCase(func() string { return "id" })
71+ ctx := context.Background()
72+
73+ top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}
74+ bottom := &entity.PaneNode{ID: "bottom", Pane: &entity.Pane{ID: "bottom"}}
75+ root := &entity.PaneNode{
76+ ID: "split",
77+ SplitDir: entity.SplitVertical,
78+ SplitRatio: 0.5,
79+ Children: []*entity.PaneNode{top, bottom},
80+ }
81+ top.Parent = root
82+ bottom.Parent = root
83+
84+ ws := &entity.Workspace{Root: root, ActivePaneID: "bottom"}
85+
86+ // Smart increase should grow the active (bottom) pane: decrease SplitRatio.
87+ if err := uc.Resize(ctx, ws, bottom, ResizeIncrease, 5.0, 10.0); err != nil {
88+ t.Fatalf("unexpected error: %v", err)
89+ }
90+ if got, want := root.SplitRatio, 0.45; got != want {
91+ t.Fatalf("split ratio = %v, want %v", got, want)
92+ }
93+
94+ // Smart decrease should shrink the active pane: increase SplitRatio.
95+ if err := uc.Resize(ctx, ws, bottom, ResizeDecrease, 5.0, 10.0); err != nil {
96+ t.Fatalf("unexpected error: %v", err)
97+ }
98+ if got, want := root.SplitRatio, 0.5; got != want {
99+ t.Fatalf("split ratio = %v, want %v", got, want)
100+ }
101+}
102+
103+func TestManagePanesUseCase_Resize_ClampsToMinPanePercent(t *testing.T) {
104+ uc := NewManagePanesUseCase(func() string { return "id" })
105+ ctx := context.Background()
106+
107+ left := &entity.PaneNode{ID: "left", Pane: &entity.Pane{ID: "left"}}
108+ right := &entity.PaneNode{ID: "right", Pane: &entity.Pane{ID: "right"}}
109+ root := &entity.PaneNode{
110+ ID: "split",
111+ SplitDir: entity.SplitHorizontal,
112+ SplitRatio: 0.9,
113+ Children: []*entity.PaneNode{left, right},
114+ }
115+ left.Parent = root
116+ right.Parent = root
117+
118+ ws := &entity.Workspace{Root: root, ActivePaneID: "left"}
119+
120+ // maxRatio = 1 - (minPanePercent/100) = 0.9
121+ if err := uc.Resize(ctx, ws, left, ResizeIncreaseRight, 5.0, 10.0); err != nil {
122+ t.Fatalf("unexpected error: %v", err)
123+ }
124+ if got, want := root.SplitRatio, 0.9; got != want {
125+ t.Fatalf("split ratio = %v, want %v", got, want)
126+ }
127+}
128+
129+func TestManagePanesUseCase_Resize_TargetsStackContainer(t *testing.T) {
130+ uc := NewManagePanesUseCase(func() string { return "id" })
131+ ctx := context.Background()
132+
133+ stackLeaf1 := &entity.PaneNode{ID: "s1", Pane: &entity.Pane{ID: "s1"}}
134+ stackLeaf2 := &entity.PaneNode{ID: "s2", Pane: &entity.Pane{ID: "s2"}}
135+ stack := &entity.PaneNode{
136+ ID: "stack",
137+ IsStacked: true,
138+ ActiveStackIndex: 0,
139+ Children: []*entity.PaneNode{stackLeaf1, stackLeaf2},
140+ }
141+ stackLeaf1.Parent = stack
142+ stackLeaf2.Parent = stack
143+
144+ right := &entity.PaneNode{ID: "right", Pane: &entity.Pane{ID: "right"}}
145+ root := &entity.PaneNode{
146+ ID: "split",
147+ SplitDir: entity.SplitHorizontal,
148+ SplitRatio: 0.5,
149+ Children: []*entity.PaneNode{stack, right},
150+ }
151+ stack.Parent = root
152+ right.Parent = root
153+
154+ ws := &entity.Workspace{Root: root, ActivePaneID: "s1"}
155+
156+ // Pass the leaf inside the stack; Resize should act on the split above the stack.
157+ if err := uc.Resize(ctx, ws, stackLeaf1, ResizeIncreaseRight, 5.0, 10.0); err != nil {
158+ t.Fatalf("unexpected error: %v", err)
159+ }
160+ if got, want := root.SplitRatio, 0.55; got != want {
161+ t.Fatalf("split ratio = %v, want %v", got, want)
162+ }
163+}
@@ -0,0 +1,163 @@
1+package usecase
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/domain/entity"
9+)
10+
11+func TestManagePanesUseCase_Resize_Errors(t *testing.T) {
12+ uc := NewManagePanesUseCase(func() string { return "id" })
13+
14+ ctx := context.Background()
15+
16+ err := uc.Resize(ctx, nil, nil, ResizeIncreaseDown, 5, 10)
17+ if err == nil {
18+ t.Fatalf("expected error when workspace is nil")
19+ }
20+
21+ ws := &entity.Workspace{}
22+ err = uc.Resize(ctx, ws, nil, ResizeIncreaseDown, 5, 10)
23+ if err == nil {
24+ t.Fatalf("expected error when pane node is nil")
25+ }
26+
27+ // Root nil should return ErrNothingToResize.
28+ leaf := &entity.PaneNode{ID: "p1", Pane: &entity.Pane{ID: "p1"}}
29+ err = uc.Resize(ctx, ws, leaf, ResizeIncreaseDown, 5, 10)
30+ if !errors.Is(err, ErrNothingToResize) {
31+ t.Fatalf("expected ErrNothingToResize, got %v", err)
32+ }
33+}
34+
35+func TestManagePanesUseCase_Resize_VerticalDividerMove(t *testing.T) {
36+ uc := NewManagePanesUseCase(func() string { return "id" })
37+ ctx := context.Background()
38+
39+ top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}
40+ bottom := &entity.PaneNode{ID: "bottom", Pane: &entity.Pane{ID: "bottom"}}
41+ root := &entity.PaneNode{
42+ ID: "split",
43+ SplitDir: entity.SplitVertical,
44+ SplitRatio: 0.5,
45+ Children: []*entity.PaneNode{top, bottom},
46+ }
47+ top.Parent = root
48+ bottom.Parent = root
49+
50+ ws := &entity.Workspace{Root: root, ActivePaneID: "bottom"}
51+
52+ // Moving divider down increases the first child's ratio.
53+ if err := uc.Resize(ctx, ws, bottom, ResizeIncreaseDown, 5.0, 10.0); err != nil {
54+ t.Fatalf("unexpected error: %v", err)
55+ }
56+ if got, want := root.SplitRatio, 0.55; got != want {
57+ t.Fatalf("split ratio = %v, want %v", got, want)
58+ }
59+
60+ // Moving divider up decreases the first child's ratio.
61+ if err := uc.Resize(ctx, ws, bottom, ResizeIncreaseUp, 5.0, 10.0); err != nil {
62+ t.Fatalf("unexpected error: %v", err)
63+ }
64+ if got, want := root.SplitRatio, 0.5; got != want {
65+ t.Fatalf("split ratio = %v, want %v", got, want)
66+ }
67+}
68+
69+func TestManagePanesUseCase_Resize_SmartResizeGrowsAndShrinksActivePane(t *testing.T) {
70+ uc := NewManagePanesUseCase(func() string { return "id" })
71+ ctx := context.Background()
72+
73+ top := &entity.PaneNode{ID: "top", Pane: &entity.Pane{ID: "top"}}
74+ bottom := &entity.PaneNode{ID: "bottom", Pane: &entity.Pane{ID: "bottom"}}
75+ root := &entity.PaneNode{
76+ ID: "split",
77+ SplitDir: entity.SplitVertical,
78+ SplitRatio: 0.5,
79+ Children: []*entity.PaneNode{top, bottom},
80+ }
81+ top.Parent = root
82+ bottom.Parent = root
83+
84+ ws := &entity.Workspace{Root: root, ActivePaneID: "bottom"}
85+
86+ // Smart increase should grow the active (bottom) pane: decrease SplitRatio.
87+ if err := uc.Resize(ctx, ws, bottom, ResizeIncrease, 5.0, 10.0); err != nil {
88+ t.Fatalf("unexpected error: %v", err)
89+ }
90+ if got, want := root.SplitRatio, 0.45; got != want {
91+ t.Fatalf("split ratio = %v, want %v", got, want)
92+ }
93+
94+ // Smart decrease should shrink the active pane: increase SplitRatio.
95+ if err := uc.Resize(ctx, ws, bottom, ResizeDecrease, 5.0, 10.0); err != nil {
96+ t.Fatalf("unexpected error: %v", err)
97+ }
98+ if got, want := root.SplitRatio, 0.5; got != want {
99+ t.Fatalf("split ratio = %v, want %v", got, want)
100+ }
101+}
102+
103+func TestManagePanesUseCase_Resize_ClampsToMinPanePercent(t *testing.T) {
104+ uc := NewManagePanesUseCase(func() string { return "id" })
105+ ctx := context.Background()
106+
107+ left := &entity.PaneNode{ID: "left", Pane: &entity.Pane{ID: "left"}}
108+ right := &entity.PaneNode{ID: "right", Pane: &entity.Pane{ID: "right"}}
109+ root := &entity.PaneNode{
110+ ID: "split",
111+ SplitDir: entity.SplitHorizontal,
112+ SplitRatio: 0.9,
113+ Children: []*entity.PaneNode{left, right},
114+ }
115+ left.Parent = root
116+ right.Parent = root
117+
118+ ws := &entity.Workspace{Root: root, ActivePaneID: "left"}
119+
120+ // maxRatio = 1 - (minPanePercent/100) = 0.9
121+ if err := uc.Resize(ctx, ws, left, ResizeIncreaseRight, 5.0, 10.0); err != nil {
122+ t.Fatalf("unexpected error: %v", err)
123+ }
124+ if got, want := root.SplitRatio, 0.9; got != want {
125+ t.Fatalf("split ratio = %v, want %v", got, want)
126+ }
127+}
128+
129+func TestManagePanesUseCase_Resize_TargetsStackContainer(t *testing.T) {
130+ uc := NewManagePanesUseCase(func() string { return "id" })
131+ ctx := context.Background()
132+
133+ stackLeaf1 := &entity.PaneNode{ID: "s1", Pane: &entity.Pane{ID: "s1"}}
134+ stackLeaf2 := &entity.PaneNode{ID: "s2", Pane: &entity.Pane{ID: "s2"}}
135+ stack := &entity.PaneNode{
136+ ID: "stack",
137+ IsStacked: true,
138+ ActiveStackIndex: 0,
139+ Children: []*entity.PaneNode{stackLeaf1, stackLeaf2},
140+ }
141+ stackLeaf1.Parent = stack
142+ stackLeaf2.Parent = stack
143+
144+ right := &entity.PaneNode{ID: "right", Pane: &entity.Pane{ID: "right"}}
145+ root := &entity.PaneNode{
146+ ID: "split",
147+ SplitDir: entity.SplitHorizontal,
148+ SplitRatio: 0.5,
149+ Children: []*entity.PaneNode{stack, right},
150+ }
151+ stack.Parent = root
152+ right.Parent = root
153+
154+ ws := &entity.Workspace{Root: root, ActivePaneID: "s1"}
155+
156+ // Pass the leaf inside the stack; Resize should act on the split above the stack.
157+ if err := uc.Resize(ctx, ws, stackLeaf1, ResizeIncreaseRight, 5.0, 10.0); err != nil {
158+ t.Fatalf("unexpected error: %v", err)
159+ }
160+ if got, want := root.SplitRatio, 0.55; got != want {
161+ t.Fatalf("split ratio = %v, want %v", got, want)
162+ }
163+}
M internal/ui/app.go +6 -6
@@ -953,16 +953,16 @@ func (a *App) applyResizeModeBorder(ctx context.Context, ws *entity.Workspace) {
953953 return
954954 }
955955
956- if a.resizeModeBorderTarget != nil && a.resizeModeBorderTarget != target {
957- a.resizeModeBorderTarget.RemoveCssClass("resize-mode-active")
958- }
959956 if !target.HasCssClass("resize-mode-active") {
960957 target.AddCssClass("resize-mode-active")
961958 }
962- if a.resizeModeBorderTarget == nil {
963- logging.FromContext(ctx).Debug().Str("pane_id", string(paneID)).Msg("resize mode border attached")
964- }
959+
965960 if a.resizeModeBorderTarget != target {
961+ if a.resizeModeBorderTarget != nil {
962+ a.resizeModeBorderTarget.RemoveCssClass("resize-mode-active")
963+ } else {
964+ logging.FromContext(ctx).Debug().Str("pane_id", string(paneID)).Msg("resize mode border attached")
965+ }
966966 a.resizeModeBorderTarget = target
967967 }
968968 }
@@ -953,16 +953,16 @@ func (a *App) applyResizeModeBorder(ctx context.Context, ws *entity.Workspace) {
953 return953 return
954 }954 }
955 955
956- if a.resizeModeBorderTarget != nil && a.resizeModeBorderTarget != target {
957- a.resizeModeBorderTarget.RemoveCssClass("resize-mode-active")
958- }
959 if !target.HasCssClass("resize-mode-active") {956 if !target.HasCssClass("resize-mode-active") {
960 target.AddCssClass("resize-mode-active")957 target.AddCssClass("resize-mode-active")
961 }958 }
962- if a.resizeModeBorderTarget == nil {959+
963- logging.FromContext(ctx).Debug().Str("pane_id", string(paneID)).Msg("resize mode border attached")
964- }
965 if a.resizeModeBorderTarget != target {960 if a.resizeModeBorderTarget != target {
961+ if a.resizeModeBorderTarget != nil {
962+ a.resizeModeBorderTarget.RemoveCssClass("resize-mode-active")
963+ } else {
964+ logging.FromContext(ctx).Debug().Str("pane_id", string(paneID)).Msg("resize mode border attached")
965+ }
966 a.resizeModeBorderTarget = target966 a.resizeModeBorderTarget = target
967 }967 }
968 }968 }

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