v0.20.1 d9a391f

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

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