v0.26.0 ae3a77a

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(uint32(0)).Once()
33
34	// Since allocation is 0, ApplyRatio returns false and callbacks are registered
35	mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(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}

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