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().SetVisible(true).Once()
50 mockEndChild.EXPECT().SetVisible(true).Once()
51 mockPaned.EXPECT().SetStartChild(mockStartChild).Once()
52 mockPaned.EXPECT().SetEndChild(mockEndChild).Once()
53
54 // Act
55 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, mockStartChild, mockEndChild, 0.5)
56
57 // Assert
58 require.NotNil(t, sv)
59 assert.Equal(t, layout.OrientationHorizontal, sv.Orientation())
60 assert.InDelta(t, 0.5, sv.GetRatio(), 0.0001)
61 assert.Equal(t, mockStartChild, sv.StartChild())
62 assert.Equal(t, mockEndChild, sv.EndChild())
63}
64
65func TestNewSplitView_Vertical(t *testing.T) {
66 // Arrange
67 ctx := context.Background()
68 mockFactory := mocks.NewMockWidgetFactory(t)
69 mockPaned := mocks.NewMockPanedWidget(t)
70 mockStartChild := mocks.NewMockWidget(t)
71 mockEndChild := mocks.NewMockWidget(t)
72
73 mockFactory.EXPECT().NewPaned(layout.OrientationVertical).Return(mockPaned).Once()
74 setupSplitViewMocks(mockPaned, layout.OrientationVertical)
75 mockStartChild.EXPECT().SetVisible(true).Once()
76 mockEndChild.EXPECT().SetVisible(true).Once()
77 mockPaned.EXPECT().SetStartChild(mockStartChild).Once()
78 mockPaned.EXPECT().SetEndChild(mockEndChild).Once()
79
80 // Act
81 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationVertical, mockStartChild, mockEndChild, 0.5)
82
83 // Assert
84 require.NotNil(t, sv)
85 assert.Equal(t, layout.OrientationVertical, sv.Orientation())
86 assert.InDelta(t, 0.5, sv.GetRatio(), 0.0001)
87 assert.Equal(t, mockStartChild, sv.StartChild())
88 assert.Equal(t, mockEndChild, sv.EndChild())
89}
90
91func TestNewSplitView_NilChildren(t *testing.T) {
92 // Arrange
93 ctx := context.Background()
94 mockFactory := mocks.NewMockWidgetFactory(t)
95 mockPaned := mocks.NewMockPanedWidget(t)
96
97 mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
98 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
99 // SetStartChild and SetEndChild should NOT be called when children are nil
100
101 // Act
102 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
103
104 // Assert
105 require.NotNil(t, sv)
106 assert.Nil(t, sv.StartChild())
107 assert.Nil(t, sv.EndChild())
108}
109
110func TestSetRatio_ValidRange(t *testing.T) {
111 tests := []struct {
112 name string
113 input float64
114 expected float64
115 }{
116 {"zero", 0.0, 0.0},
117 {"one", 1.0, 1.0},
118 {"middle", 0.5, 0.5},
119 {"quarter", 0.25, 0.25},
120 {"three_quarters", 0.75, 0.75},
121 }
122
123 for _, tt := range tests {
124 t.Run(tt.name, func(t *testing.T) {
125 ctx := context.Background()
126 // Arrange
127 mockFactory := mocks.NewMockWidgetFactory(t)
128 mockPaned := mocks.NewMockPanedWidget(t)
129
130 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
131 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
132
133 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
134
135 // Act
136 sv.SetRatio(tt.input)
137
138 // Assert
139 assert.InDelta(t, tt.expected, sv.GetRatio(), 0.0001)
140 })
141 }
142}
143
144func TestSetRatio_OutOfRange_Clamped(t *testing.T) {
145 tests := []struct {
146 name string
147 input float64
148 expected float64
149 }{
150 {"negative", -0.5, 0.0},
151 {"very_negative", -100.0, 0.0},
152 {"greater_than_one", 1.5, 1.0},
153 {"much_greater", 100.0, 1.0},
154 }
155
156 for _, tt := range tests {
157 t.Run(tt.name, func(t *testing.T) {
158 ctx := context.Background()
159 // Arrange
160 mockFactory := mocks.NewMockWidgetFactory(t)
161 mockPaned := mocks.NewMockPanedWidget(t)
162
163 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
164 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
165
166 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
167
168 // Act
169 sv.SetRatio(tt.input)
170
171 // Assert
172 assert.InDelta(t, tt.expected, sv.GetRatio(), 0.0001)
173 })
174 }
175}
176
177func TestSwapStart_ReplacesChild(t *testing.T) {
178 // Arrange
179 ctx := context.Background()
180 mockFactory := mocks.NewMockWidgetFactory(t)
181 mockPaned := mocks.NewMockPanedWidget(t)
182 mockOldChild := mocks.NewMockWidget(t)
183 mockNewChild := mocks.NewMockWidget(t)
184
185 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
186 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
187 mockOldChild.EXPECT().SetVisible(true).Once()
188 mockPaned.EXPECT().SetStartChild(mockOldChild).Once()
189
190 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, mockOldChild, nil, 0.5)
191
192 // Expect swap operations
193 mockPaned.EXPECT().SetStartChild(nil).Once() // Remove old
194 mockPaned.EXPECT().SetStartChild(mockNewChild).Once() // Add new
195
196 // Act
197 sv.SwapStart(mockNewChild)
198
199 // Assert
200 assert.Equal(t, mockNewChild, sv.StartChild())
201}
202
203func TestSwapEnd_ReplacesChild(t *testing.T) {
204 // Arrange
205 ctx := context.Background()
206 mockFactory := mocks.NewMockWidgetFactory(t)
207 mockPaned := mocks.NewMockPanedWidget(t)
208 mockOldChild := mocks.NewMockWidget(t)
209 mockNewChild := mocks.NewMockWidget(t)
210
211 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
212 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
213 mockOldChild.EXPECT().SetVisible(true).Once()
214 mockPaned.EXPECT().SetEndChild(mockOldChild).Once()
215
216 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, mockOldChild, 0.5)
217
218 // Expect swap operations
219 mockPaned.EXPECT().SetEndChild(nil).Once() // Remove old
220 mockPaned.EXPECT().SetEndChild(mockNewChild).Once() // Add new
221
222 // Act
223 sv.SwapEnd(mockNewChild)
224
225 // Assert
226 assert.Equal(t, mockNewChild, sv.EndChild())
227}
228
229func TestSwapStart_FromNil(t *testing.T) {
230 // Arrange
231 ctx := context.Background()
232 mockFactory := mocks.NewMockWidgetFactory(t)
233 mockPaned := mocks.NewMockPanedWidget(t)
234 mockNewChild := mocks.NewMockWidget(t)
235
236 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
237 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
238
239 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
240
241 // Expect only adding new child (no removal since old was nil)
242 mockPaned.EXPECT().SetStartChild(mockNewChild).Once()
243
244 // Act
245 sv.SwapStart(mockNewChild)
246
247 // Assert
248 assert.Equal(t, mockNewChild, sv.StartChild())
249}
250
251func TestSwapEnd_ToNil(t *testing.T) {
252 // Arrange
253 ctx := context.Background()
254 mockFactory := mocks.NewMockWidgetFactory(t)
255 mockPaned := mocks.NewMockPanedWidget(t)
256 mockOldChild := mocks.NewMockWidget(t)
257
258 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
259 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
260 mockOldChild.EXPECT().SetVisible(true).Once()
261 mockPaned.EXPECT().SetEndChild(mockOldChild).Once()
262
263 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, mockOldChild, 0.5)
264
265 // Expect removal (SetEndChild(nil) called for clearing old)
266 mockPaned.EXPECT().SetEndChild(nil).Once()
267
268 // Act
269 sv.SwapEnd(nil)
270
271 // Assert
272 assert.Nil(t, sv.EndChild())
273}
274
275func TestWidget_ReturnsPanedWidget(t *testing.T) {
276 // Arrange
277 ctx := context.Background()
278 mockFactory := mocks.NewMockWidgetFactory(t)
279 mockPaned := mocks.NewMockPanedWidget(t)
280
281 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
282 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
283
284 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
285
286 // Act
287 widget := sv.Widget()
288
289 // Assert
290 assert.Equal(t, mockPaned, widget)
291}
292
293func TestSetPosition_DelegatesToPaned(t *testing.T) {
294 // Arrange
295 ctx := context.Background()
296 mockFactory := mocks.NewMockWidgetFactory(t)
297 mockPaned := mocks.NewMockPanedWidget(t)
298
299 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
300 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
301
302 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
303
304 mockPaned.EXPECT().SetPosition(400).Once()
305
306 // Act
307 sv.SetPosition(400)
308
309 // Assert - verified by mock expectations
310}
311
312func TestGetPosition_DelegatesToPaned(t *testing.T) {
313 // Arrange
314 ctx := context.Background()
315 mockFactory := mocks.NewMockWidgetFactory(t)
316 mockPaned := mocks.NewMockPanedWidget(t)
317
318 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
319 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
320
321 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
322
323 mockPaned.EXPECT().GetPosition().Return(400).Once()
324
325 // Act
326 pos := sv.GetPosition()
327
328 // Assert
329 assert.Equal(t, 400, pos)
330}
331
332func TestSetWideHandle_DelegatesToPaned(t *testing.T) {
333 // Arrange
334 ctx := context.Background()
335 mockFactory := mocks.NewMockWidgetFactory(t)
336 mockPaned := mocks.NewMockPanedWidget(t)
337
338 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
339 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
340
341 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, 0.5)
342
343 mockPaned.EXPECT().SetWideHandle(true).Once()
344
345 // Act
346 sv.SetWideHandle(true)
347
348 // Assert - verified by mock expectations
349}
350
351func TestNewSplitView_InitialRatio_Clamped(t *testing.T) {
352 tests := []struct {
353 name string
354 input float64
355 expected float64
356 }{
357 {"negative_clamped_to_zero", -0.5, 0.0},
358 {"greater_than_one_clamped", 1.5, 1.0},
359 {"valid_preserved", 0.3, 0.3},
360 }
361
362 for _, tt := range tests {
363 t.Run(tt.name, func(t *testing.T) {
364 ctx := context.Background()
365 // Arrange
366 mockFactory := mocks.NewMockWidgetFactory(t)
367 mockPaned := mocks.NewMockPanedWidget(t)
368
369 mockFactory.EXPECT().NewPaned(mock.Anything).Return(mockPaned).Once()
370 setupSplitViewMocks(mockPaned, layout.OrientationHorizontal)
371
372 // Act
373 sv := layout.NewSplitView(ctx, mockFactory, layout.OrientationHorizontal, nil, nil, tt.input)
374
375 // Assert
376 assert.InDelta(t, tt.expected, sv.GetRatio(), 0.0001)
377 })
378 }
379}