1package layout_test
2
3import (
4 "context"
5 "testing"
6
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/mock"
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/dumber/internal/ui/layout"
12 "github.com/bnema/dumber/internal/ui/layout/mocks"
13)
14
15// setupSplitViewMocks configures the common mock expectations for NewSplitView.
16// Since the mock paned returns 0 for GetAllocatedWidth/Height, ApplyRatio will
17// return false and trigger the ConnectMap and AddTickCallback registrations.
18func setupSplitViewMocks(mockPaned *mocks.MockPanedWidget, orientation layout.Orientation) {
19 mockPaned.EXPECT().SetResizeStartChild(true).Once()
20 mockPaned.EXPECT().SetResizeEndChild(true).Once()
21 mockPaned.EXPECT().SetVisible(true).Once()
22 // Note: SetShrinkStartChild/SetShrinkEndChild are NOT called - allowing shrink
23 // enables GTK to respect the 50/50 ratio even if children have larger natural sizes
24
25 // ApplyRatio is called immediately, which calls GetAllocatedWidth/Height
26 if orientation == layout.OrientationHorizontal {
27 mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
28 } else {
29 mockPaned.EXPECT().GetAllocatedHeight().Return(0).Once()
30 }
31
32 mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint(0)).Once()
33
34 // Since allocation is 0, ApplyRatio returns false and callbacks are registered
35 mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint(0)).Once()
36 mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
37}
38
39func TestNewSplitView_Horizontal(t *testing.T) {
40 // Arrange
41 ctx := context.Background()
42 mockFactory := mocks.NewMockWidgetFactory(t)
43 mockPaned := mocks.NewMockPanedWidget(t)
44 mockStartChild := mocks.NewMockWidget(t)
45 mockEndChild := mocks.NewMockWidget(t)
46
47 mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
48 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
49 mockStartChild.EXPECT().GetAllocatedWidth().Return(0).Maybe()
50 mockStartChild.EXPECT().GetAllocatedHeight().Return(0).Maybe()
51 mockEndChild.EXPECT().GetAllocatedWidth().Return(0).Maybe()
52 mockEndChild.EXPECT().GetAllocatedHeight().Return(0).Maybe()
53 mockStartChild.EXPECT().SetVisible(true).Once()
54 mockEndChild.EXPECT().SetVisible(true).Once()
55 mockPaned.EXPECT().SetStartChild(mockStartChild).Once()
56 mockPaned.EXPECT().SetEndChild(mockEndChild).Once()
57
58 // Act
59 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, mockStartChild, mockEndChild, 0.5)
60
61 // Assert
62 require.NotNil(t, sv)
63 assert.Equal(t, layout.OrientationHorizontal, sv.Orientation())
64 assert.InDelta(t, 0.5, sv.GetRatio(), 0.0001)
65 assert.Equal(t, mockStartChild, sv.StartChild())
66 assert.Equal(t, mockEndChild, sv.EndChild())
67}
68
69func TestNewSplitView_Vertical(t *testing.T) {
70 // Arrange
71 ctx := context.Background()
72 mockFactory := mocks.NewMockWidgetFactory(t)
73 mockPaned := mocks.NewMockPanedWidget(t)
74 mockStartChild := mocks.NewMockWidget(t)
75 mockEndChild := mocks.NewMockWidget(t)
76
77 mockFactory.EXPECT().NewPaned(layout.OrientationVertical).Return(mockPaned).Once()
78 setupSplitViewMocks(mockPaned, layout.OrientationVertical)
79 mockStartChild.EXPECT().GetAllocatedWidth().Return(0).Maybe()
80 mockStartChild.EXPECT().GetAllocatedHeight().Return(0).Maybe()
81 mockEndChild.EXPECT().GetAllocatedWidth().Return(0).Maybe()
82 mockEndChild.EXPECT().GetAllocatedHeight().Return(0).Maybe()
83 mockStartChild.EXPECT().SetVisible(true).Once()
84 mockEndChild.EXPECT().SetVisible(true).Once()
85 mockPaned.EXPECT().SetStartChild(mockStartChild).Once()
86 mockPaned.EXPECT().SetEndChild(mockEndChild).Once()
87
88 // Act
89 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationVertical, mockStartChild, mockEndChild, 0.5)
90
91 // Assert
92 require.NotNil(t, sv)
93 assert.Equal(t, layout.OrientationVertical, sv.Orientation())
94 assert.InDelta(t, 0.5, sv.GetRatio(), 0.0001)
95 assert.Equal(t, mockStartChild, sv.StartChild())
96 assert.Equal(t, mockEndChild, sv.EndChild())
97}
98
99func TestNewSplitView_NilChildren(t *testing.T) {
100 // Arrange
101 ctx := context.Background()
102 mockFactory := mocks.NewMockWidgetFactory(t)
103 mockPaned := mocks.NewMockPanedWidget(t)
104
105 mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
106 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
107 // SetStartChild and SetEndChild should NOT be called when children are nil
108
109 // Act
110 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
111
112 // Assert
113 require.NotNil(t, sv)
114 assert.Nil(t, sv.StartChild())
115 assert.Nil(t, sv.EndChild())
116}
117
118func TestSetRatio_ValidRange(t *testing.T) {
119 tests := []struct {
120 name string
121 input float64
122 expected float64
123 }{
124 {"zero", 0.0, 0.0},
125 {"one", 1.0, 1.0},
126 {"middle", 0.5, 0.5},
127 {"quarter", 0.25, 0.25},
128 {"three_quarters", 0.75, 0.75},
129 }
130
131 for _, tt := range tests {
132 t.Run(tt.name, func(t *testing.T) {
133 ctx := context.Background()
134 // Arrange
135 mockFactory := mocks.NewMockWidgetFactory(t)
136 mockPaned := mocks.NewMockPanedWidget(t)
137
138 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
139 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
140
141 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
142
143 // Act
144 sv.SetRatio(tt.input)
145
146 // Assert
147 assert.InDelta(t, tt.expected, sv.GetRatio(), 0.0001)
148 })
149 }
150}
151
152func TestSetRatio_OutOfRange_Clamped(t *testing.T) {
153 tests := []struct {
154 name string
155 input float64
156 expected float64
157 }{
158 {"negative", -0.5, 0.0},
159 {"very_negative", -100.0, 0.0},
160 {"greater_than_one", 1.5, 1.0},
161 {"much_greater", 100.0, 1.0},
162 }
163
164 for _, tt := range tests {
165 t.Run(tt.name, func(t *testing.T) {
166 ctx := context.Background()
167 // Arrange
168 mockFactory := mocks.NewMockWidgetFactory(t)
169 mockPaned := mocks.NewMockPanedWidget(t)
170
171 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
172 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
173
174 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
175
176 // Act
177 sv.SetRatio(tt.input)
178
179 // Assert
180 assert.InDelta(t, tt.expected, sv.GetRatio(), 0.0001)
181 })
182 }
183}
184
185func TestSwapStart_ReplacesChild(t *testing.T) {
186 // Arrange
187 ctx := context.Background()
188 mockFactory := mocks.NewMockWidgetFactory(t)
189 mockPaned := mocks.NewMockPanedWidget(t)
190 mockOldChild := mocks.NewMockWidget(t)
191 mockNewChild := mocks.NewMockWidget(t)
192
193 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
194 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
195 mockOldChild.EXPECT().GetAllocatedWidth().Return(0).Maybe()
196 mockOldChild.EXPECT().GetAllocatedHeight().Return(0).Maybe()
197 mockOldChild.EXPECT().SetVisible(true).Once()
198 mockPaned.EXPECT().SetStartChild(mockOldChild).Once()
199
200 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, mockOldChild, nil, 0.5)
201
202 // Expect swap operations
203 mockPaned.EXPECT().SetStartChild(nil).Once() // Remove old
204 mockPaned.EXPECT().SetStartChild(mockNewChild).Once() // Add new
205
206 // Act
207 sv.SwapStart(mockNewChild)
208
209 // Assert
210 assert.Equal(t, mockNewChild, sv.StartChild())
211}
212
213func TestSwapEnd_ReplacesChild(t *testing.T) {
214 // Arrange
215 ctx := context.Background()
216 mockFactory := mocks.NewMockWidgetFactory(t)
217 mockPaned := mocks.NewMockPanedWidget(t)
218 mockOldChild := mocks.NewMockWidget(t)
219 mockNewChild := mocks.NewMockWidget(t)
220
221 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
222 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
223 mockOldChild.EXPECT().GetAllocatedWidth().Return(0).Maybe()
224 mockOldChild.EXPECT().GetAllocatedHeight().Return(0).Maybe()
225 mockOldChild.EXPECT().SetVisible(true).Once()
226 mockPaned.EXPECT().SetEndChild(mockOldChild).Once()
227
228 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, mockOldChild, 0.5)
229
230 // Expect swap operations
231 mockPaned.EXPECT().SetEndChild(nil).Once() // Remove old
232 mockPaned.EXPECT().SetEndChild(mockNewChild).Once() // Add new
233
234 // Act
235 sv.SwapEnd(mockNewChild)
236
237 // Assert
238 assert.Equal(t, mockNewChild, sv.EndChild())
239}
240
241func TestSwapStart_FromNil(t *testing.T) {
242 // Arrange
243 ctx := context.Background()
244 mockFactory := mocks.NewMockWidgetFactory(t)
245 mockPaned := mocks.NewMockPanedWidget(t)
246 mockNewChild := mocks.NewMockWidget(t)
247
248 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
249 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
250
251 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
252
253 // Expect only adding new child (no removal since old was nil)
254 mockPaned.EXPECT().SetStartChild(mockNewChild).Once()
255
256 // Act
257 sv.SwapStart(mockNewChild)
258
259 // Assert
260 assert.Equal(t, mockNewChild, sv.StartChild())
261}
262
263func TestSwapEnd_ToNil(t *testing.T) {
264 // Arrange
265 ctx := context.Background()
266 mockFactory := mocks.NewMockWidgetFactory(t)
267 mockPaned := mocks.NewMockPanedWidget(t)
268 mockOldChild := mocks.NewMockWidget(t)
269
270 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
271 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
272 mockOldChild.EXPECT().GetAllocatedWidth().Return(0).Maybe()
273 mockOldChild.EXPECT().GetAllocatedHeight().Return(0).Maybe()
274 mockOldChild.EXPECT().SetVisible(true).Once()
275 mockPaned.EXPECT().SetEndChild(mockOldChild).Once()
276
277 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, mockOldChild, 0.5)
278
279 // Expect removal (SetEndChild(nil) called for clearing old)
280 mockPaned.EXPECT().SetEndChild(nil).Once()
281
282 // Act
283 sv.SwapEnd(nil)
284
285 // Assert
286 assert.Nil(t, sv.EndChild())
287}
288
289func TestWidget_ReturnsPanedWidget(t *testing.T) {
290 // Arrange
291 ctx := context.Background()
292 mockFactory := mocks.NewMockWidgetFactory(t)
293 mockPaned := mocks.NewMockPanedWidget(t)
294
295 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
296 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
297
298 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
299
300 // Act
301 widget := sv.Widget()
302
303 // Assert
304 assert.Equal(t, mockPaned, widget)
305}
306
307func TestSetPosition_DelegatesToPaned(t *testing.T) {
308 // Arrange
309 ctx := context.Background()
310 mockFactory := mocks.NewMockWidgetFactory(t)
311 mockPaned := mocks.NewMockPanedWidget(t)
312
313 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
314 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
315
316 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
317
318 mockPaned.EXPECT().SetPosition(400).Once()
319
320 // Act
321 sv.SetPosition(400)
322
323 // Assert - verified by mock expectations
324}
325
326func TestApplyRatio_Vertical_ContinuesWhenChildrenStayDegenerate(t *testing.T) {
327 ctx := context.Background()
328 mockFactory := mocks.NewMockWidgetFactory(t)
329 mockPaned := mocks.NewMockPanedWidget(t)
330 mockStartChild := mocks.NewMockWidget(t)
331 mockEndChild := mocks.NewMockWidget(t)
332
333 mockFactory.EXPECT().NewPaned(layout.OrientationVertical).Return(mockPaned).Once()
334 setupSplitViewMocks(mockPaned, layout.OrientationVertical)
335 mockStartChild.EXPECT().GetAllocatedWidth().Return(0).Maybe()
336 mockStartChild.EXPECT().GetAllocatedHeight().Return(2143).Twice()
337 mockStartChild.EXPECT().SetVisible(true).Once()
338 mockEndChild.EXPECT().GetAllocatedWidth().Return(0).Maybe()
339 mockEndChild.EXPECT().GetAllocatedHeight().Return(1).Twice()
340 mockEndChild.EXPECT().SetVisible(true).Once()
341 mockPaned.EXPECT().SetStartChild(mockStartChild).Once()
342 mockPaned.EXPECT().SetEndChild(mockEndChild).Once()
343
344 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationVertical, mockStartChild, mockEndChild, 0.5)
345 require.NotNil(t, sv)
346
347 mockPaned.EXPECT().GetAllocatedHeight().Return(2144).Once()
348 mockPaned.EXPECT().GetPosition().Return(0).Once()
349 mockPaned.EXPECT().SetPosition(1072).Once()
350 mockPaned.EXPECT().GetPosition().Return(1072).Once()
351
352 assert.False(t, sv.ApplyRatio())
353}
354
355func TestGetPosition_DelegatesToPaned(t *testing.T) {
356 // Arrange
357 ctx := context.Background()
358 mockFactory := mocks.NewMockWidgetFactory(t)
359 mockPaned := mocks.NewMockPanedWidget(t)
360
361 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
362 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
363
364 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
365
366 mockPaned.EXPECT().GetPosition().Return(400).Once()
367
368 // Act
369 pos := sv.GetPosition()
370
371 // Assert
372 assert.Equal(t, 400, pos)
373}
374
375func TestSetWideHandle_DelegatesToPaned(t *testing.T) {
376 // Arrange
377 ctx := context.Background()
378 mockFactory := mocks.NewMockWidgetFactory(t)
379 mockPaned := mocks.NewMockPanedWidget(t)
380
381 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
382 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
383
384 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
385
386 mockPaned.EXPECT().SetWideHandle(true).Once()
387
388 // Act
389 sv.SetWideHandle(true)
390
391 // Assert - verified by mock expectations
392}
393
394func TestNewSplitView_InitialRatio_Clamped(t *testing.T) {
395 tests := []struct {
396 name string
397 input float64
398 expected float64
399 }{
400 {"negative_clamped_to_zero", -0.5, 0.0},
401 {"greater_than_one_clamped", 1.5, 1.0},
402 {"valid_preserved", 0.3, 0.3},
403 }
404
405 for _, tt := range tests {
406 t.Run(tt.name, func(t *testing.T) {
407 ctx := context.Background()
408 // Arrange
409 mockFactory := mocks.NewMockWidgetFactory(t)
410 mockPaned := mocks.NewMockPanedWidget(t)
411
412 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
413 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
414
415 // Act
416 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, tt.input)
417
418 // Assert
419 assert.InDelta(t, tt.expected, sv.GetRatio(), 0.0001)
420 })
421 }
422}