1package layout
2
3import (
4 "testing"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/stretchr/testify/require"
8)
9
10func TestResizeFocusTransfersSolvedExtent(t *testing.T) {
11 t.Parallel()
12
13 tests := []struct {
14 name string
15 root *Node
16 focus PaneID
17 axis Axis
18 delta int
19 area domain.Rect
20 want map[PaneID]domain.Rect
21 }{
22 {
23 name: "grow width takes from right neighbor",
24 root: split(Horizontal, NewLeaf("a"), NewLeaf("b")),
25 focus: "a",
26 axis: Width,
27 delta: 5,
28 area: domain.Rect{Width: 51, Height: 2},
29 want: map[PaneID]domain.Rect{
30 "a": {Width: 30, Height: 2},
31 "b": {X: 31, Width: 20, Height: 2},
32 },
33 },
34 {
35 name: "shrink width gives to right neighbor",
36 root: split(Horizontal, weightedLeaf("a", 30), weightedLeaf("b", 20)),
37 focus: "a",
38 axis: Width,
39 delta: -5,
40 area: domain.Rect{Width: 51, Height: 2},
41 want: map[PaneID]domain.Rect{
42 "a": {Width: 25, Height: 2},
43 "b": {X: 26, Width: 25, Height: 2},
44 },
45 },
46 {
47 name: "grow height takes from bottom neighbor",
48 root: split(Vertical,
49 weightedLeaf("a", 4), weightedLeaf("b", 3),
50 ),
51 focus: "a",
52 axis: Height,
53 delta: 1,
54 area: domain.Rect{Width: 20, Height: 8},
55 want: map[PaneID]domain.Rect{
56 "a": {Width: 20, Height: 5},
57 "b": {Y: 6, Width: 20, Height: 2},
58 },
59 },
60 }
61
62 for _, tt := range tests {
63 t.Run(tt.name, func(t *testing.T) {
64 tr := &Tree{Root: tt.root, Focus: tt.focus}
65 require.NoError(t, tr.ResizeFocus(tt.axis, tt.delta, tt.area))
66 placements, ok := Solve(tr.Root, tt.area)
67 require.True(t, ok)
68 require.Equal(t, tt.want, contents(placements))
69 })
70 }
71}
72
73func TestResizeFocusSelectsNearestTargetAndAdjacentSibling(t *testing.T) {
74 t.Parallel()
75
76 t.Run("nearest matching ancestor", func(t *testing.T) {
77 inner := split(Horizontal, NewLeaf("a"), NewLeaf("b"))
78 tr := &Tree{Root: split(Horizontal, weightedNode(inner, 2), weightedLeaf("c", 1)), Focus: "a"}
79 area := domain.Rect{Width: 122, Height: 2}
80 before, ok := Solve(tr.Root, area)
81 require.True(t, ok)
82
83 require.NoError(t, tr.ResizeFocus(Width, 5, area))
84 after, ok := Solve(tr.Root, area)
85 require.True(t, ok)
86 require.Equal(t, contents(before)["c"], contents(after)["c"])
87 require.Equal(t, contents(before)["a"].Width+5, contents(after)["a"].Width)
88 require.Equal(t, contents(before)["b"].Width-5, contents(after)["b"].Width)
89 })
90
91 t.Run("direct child subtree is resized as one target", func(t *testing.T) {
92 subtree := split(Vertical, NewLeaf("a"), NewLeaf("d"))
93 tr := &Tree{Root: split(Horizontal, subtree, NewLeaf("b")), Focus: "a"}
94 area := domain.Rect{Width: 51, Height: 5}
95 require.NoError(t, tr.ResizeFocus(Width, 5, area))
96 placements, ok := Solve(tr.Root, area)
97 require.True(t, ok)
98 require.Equal(t, 30, contents(placements)["a"].Width)
99 require.Equal(t, 30, contents(placements)["d"].Width)
100 require.Equal(t, 20, contents(placements)["b"].Width)
101 })
102
103 t.Run("stack is resized as one target", func(t *testing.T) {
104 stackNode := &Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("d")}, Expanded: "a"}
105 tr := &Tree{Root: split(Horizontal, stackNode, NewLeaf("b")), Focus: "d"}
106 area := domain.Rect{Width: 51, Height: 3}
107 require.NoError(t, tr.ResizeFocus(Width, 5, area))
108 placements, ok := Solve(tr.Root, area)
109 require.True(t, ok)
110 require.Equal(t, 30, contents(placements)["a"].Width)
111 var collapsedTitleWidth int
112 for _, placement := range placements {
113 if placement.ID == "d" {
114 collapsedTitleWidth = placement.TitleBar.Width
115 }
116 }
117 require.Equal(t, 30, collapsedTitleWidth)
118 require.Equal(t, 20, contents(placements)["b"].Width)
119 })
120
121 t.Run("right donor at minimum falls back to left", func(t *testing.T) {
122 tr := &Tree{Root: split(Horizontal,
123 weightedLeaf("a", 40), weightedLeaf("b", 20), weightedLeaf("c", 20),
124 ), Focus: "b"}
125 area := domain.Rect{Width: 82, Height: 2}
126 require.NoError(t, tr.ResizeFocus(Width, 5, area))
127 placements, ok := Solve(tr.Root, area)
128 require.True(t, ok)
129 require.Equal(t, map[PaneID]int{"a": 35, "b": 25, "c": 20}, placementWidths(placements))
130 })
131
132 t.Run("last target falls back to left neighbor", func(t *testing.T) {
133 tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 30), weightedLeaf("b", 20)), Focus: "b"}
134 area := domain.Rect{Width: 51, Height: 2}
135 require.NoError(t, tr.ResizeFocus(Width, 5, area))
136 placements, ok := Solve(tr.Root, area)
137 require.True(t, ok)
138 require.Equal(t, map[PaneID]int{"a": 25, "b": 25}, placementWidths(placements))
139 })
140}
141
142func TestResizeFocusMinimumAndTransactionalFailures(t *testing.T) {
143 t.Parallel()
144
145 minInt := -int(^uint(0)>>1) - 1
146 tests := []struct {
147 name string
148 tree *Tree
149 axis Axis
150 delta int
151 area domain.Rect
152 err error
153 want map[PaneID]int
154 }{
155 {
156 name: "partial growth uses every available donor cell",
157 tree: &Tree{Root: split(Horizontal, weightedLeaf("a", 20), weightedLeaf("b", 22)), Focus: "a"},
158 axis: Width,
159 delta: 5,
160 area: domain.Rect{Width: 43, Height: 2},
161 want: map[PaneID]int{"a": 22, "b": 20},
162 },
163 {
164 name: "minimum integer clamps without overflow",
165 tree: &Tree{Root: split(Horizontal, weightedLeaf("a", 30), weightedLeaf("b", 20)), Focus: "a"},
166 axis: Width,
167 delta: minInt,
168 area: domain.Rect{Width: 51, Height: 2},
169 want: map[PaneID]int{"a": 20, "b": 30},
170 },
171 {
172 name: "partial growth stops at recursive donor minimum",
173 tree: &Tree{Root: split(Horizontal,
174 weightedLeaf("a", 20),
175 weightedNode(split(Horizontal, NewLeaf("b"), NewLeaf("d")), 43),
176 ), Focus: "a"},
177 axis: Width,
178 delta: 5,
179 area: domain.Rect{Width: 64, Height: 2},
180 want: map[PaneID]int{"a": 22, "b": 20, "d": 20},
181 },
182 {
183 name: "no donor capacity",
184 tree: &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"},
185 axis: Width,
186 delta: 2,
187 area: domain.Rect{Width: 41, Height: 2},
188 err: ErrTooSmall,
189 },
190 {
191 name: "target cannot shrink",
192 tree: &Tree{Root: split(Horizontal, NewLeaf("a"), weightedLeaf("b", 2)), Focus: "a"},
193 axis: Width,
194 delta: -2,
195 area: domain.Rect{Width: 61, Height: 2},
196 err: ErrTooSmall,
197 },
198 {
199 name: "wrong axis",
200 tree: &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"},
201 axis: Height,
202 delta: 1,
203 area: domain.Rect{Width: 41, Height: 2},
204 err: ErrNotInSplit,
205 },
206 {
207 name: "single pane",
208 tree: NewTree("a"),
209 axis: Width,
210 delta: 2,
211 area: domain.Rect{Width: 20, Height: 2},
212 err: ErrNotInSplit,
213 },
214 {
215 name: "invalid area rolls back",
216 tree: &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"},
217 axis: Width,
218 delta: 2,
219 area: domain.Rect{Width: 20, Height: 2},
220 err: ErrTooSmall,
221 },
222 }
223
224 for _, tt := range tests {
225 t.Run(tt.name, func(t *testing.T) {
226 before := tt.tree.Clone()
227 err := tt.tree.ResizeFocus(tt.axis, tt.delta, tt.area)
228 if tt.err != nil {
229 require.ErrorIs(t, err, tt.err)
230 require.Equal(t, before, tt.tree, "failed resize must not mutate the tree")
231 return
232 }
233 require.NoError(t, err)
234 placements, ok := Solve(tt.tree.Root, tt.area)
235 require.True(t, ok)
236 require.Equal(t, tt.want, placementWidths(placements))
237 })
238 }
239}
240
241func TestResizeFocusRetainsProportionsAcrossTerminalSizes(t *testing.T) {
242 t.Parallel()
243
244 tr := &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"}
245 small := domain.Rect{Width: 81, Height: 2}
246 require.NoError(t, tr.ResizeFocus(Width, 10, small))
247
248 placements, ok := Solve(tr.Root, domain.Rect{Width: 161, Height: 2})
249 require.True(t, ok)
250 require.Equal(t, map[PaneID]int{"a": 100, "b": 60}, placementWidths(placements))
251 placements, ok = Solve(tr.Root, small)
252 require.True(t, ok)
253 require.Equal(t, map[PaneID]int{"a": 50, "b": 30}, placementWidths(placements))
254}
255
256func TestEqualize(t *testing.T) {
257 t.Parallel()
258
259 tests := []struct {
260 name string
261 tree *Tree
262 area domain.Rect
263 want map[PaneID]int
264 }{
265 {
266 name: "weighted siblings become equal",
267 tree: &Tree{Root: split(Horizontal, weightedLeaf("a", 3), weightedLeaf("b", 1)), Focus: "a"},
268 area: domain.Rect{Width: 81, Height: 2},
269 want: map[PaneID]int{"a": 40, "b": 40},
270 },
271 {
272 name: "recursive minimum constrains only the large child",
273 tree: &Tree{Root: split(Horizontal,
274 weightedNode(split(Horizontal, weightedLeaf("a", 5), weightedLeaf("b", 1)), 9),
275 weightedLeaf("c", 1), weightedLeaf("d", 1),
276 ), Focus: "a"},
277 area: domain.Rect{Width: 102, Height: 2},
278 want: map[PaneID]int{"a": 20, "b": 20, "c": 30, "d": 29},
279 },
280 }
281
282 for _, tt := range tests {
283 t.Run(tt.name, func(t *testing.T) {
284 require.NoError(t, tt.tree.Equalize(tt.area))
285 placements, ok := Solve(tt.tree.Root, tt.area)
286 require.True(t, ok)
287 require.Equal(t, tt.want, placementWidths(placements))
288 })
289 }
290}
291
292func TestEqualizeFailureIsTransactional(t *testing.T) {
293 t.Parallel()
294
295 tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 3), weightedLeaf("b", 1)), Focus: "a"}
296 before := tr.Clone()
297 require.ErrorIs(t, tr.Equalize(domain.Rect{Width: 20, Height: 2}), ErrTooSmall)
298 require.Equal(t, before, tr)
299}
300
301func TestCanResize(t *testing.T) {
302 t.Parallel()
303
304 tests := []struct {
305 name string
306 tree *Tree
307 want bool
308 }{
309 {name: "single pane", tree: NewTree("a"), want: false},
310 {name: "focused split child", tree: &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"}, want: true},
311 {name: "focused stack member under split", tree: &Tree{Root: split(Horizontal, &Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("b")}, Expanded: "a"}, NewLeaf("c")), Focus: "b"}, want: true},
312 {name: "stack without split", tree: &Tree{Root: &Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("b")}, Expanded: "a"}, Focus: "a"}, want: false},
313 {name: "missing focus", tree: &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "missing"}, want: false},
314 }
315
316 for _, tt := range tests {
317 t.Run(tt.name, func(t *testing.T) {
318 require.Equal(t, tt.want, tt.tree.CanResize())
319 })
320 }
321}