main 5041353

split_test.go RawHistoryFolder

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}

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