stacked_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// setupMockFactory creates a factory that returns configured mocks for stacked view tests.
16func setupMockFactory(t *testing.T) (*mocks.MockWidgetFactory, *mocks.MockBoxWidget) {
17	mockFactory := mocks.NewMockWidgetFactory(t)
18	mockBox := mocks.NewMockBoxWidget(t)
19
20	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBox).Once()
21	mockBox.EXPECT().SetHexpand(true).Once()
22	mockBox.EXPECT().SetVexpand(true).Once()
23
24	return mockFactory, mockBox
25}
26
27// setupPaneMocks creates mocks needed for AddPane
28// Note: The stacked view now uses GestureClick on the titleBar directly instead of wrapping it in a button.
29// The GestureClick is added via AddController which we mock to accept any EventController.
30func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox *mocks.MockBoxWidget) (
31	*mocks.MockBoxWidget, *mocks.MockImageWidget, *mocks.MockLabelWidget, *mocks.MockWidget,
32) {
33	mockTitleBar := mocks.NewMockBoxWidget(t)
34	mockFavicon := mocks.NewMockImageWidget(t)
35	mockLabel := mocks.NewMockLabelWidget(t)
36	mockCloseButton := mocks.NewMockButtonWidget(t)
37	mockContainer := mocks.NewMockWidget(t)
38
39	// Title bar creation - now directly used without button wrapper
40	mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
41	mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
42	mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
43	mockTitleBar.EXPECT().SetVexpand(false).Once()
44	mockTitleBar.EXPECT().SetHexpand(true).Once()
45
46	// Favicon
47	mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
48	mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
49	mockFavicon.EXPECT().SetPixelSize(16).Once()
50	mockTitleBar.EXPECT().Append(mockFavicon).Once()
51
52	// Label
53	mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once()
54	mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
55	mockLabel.EXPECT().SetMaxWidthChars(30).Once()
56	mockLabel.EXPECT().SetHexpand(true).Once()
57	mockLabel.EXPECT().SetXalign(float32(0.0)).Once()
58	mockTitleBar.EXPECT().Append(mockLabel).Once()
59
60	// Close button (uses SetIconName directly instead of child image)
61	mockFactory.EXPECT().NewButton().Return(mockCloseButton).Once()
62	mockCloseButton.EXPECT().SetIconName("window-close-symbolic").Once()
63	mockCloseButton.EXPECT().AddCssClass("stacked-pane-close-button").Once()
64	mockCloseButton.EXPECT().SetFocusOnClick(false).Once()
65	mockCloseButton.EXPECT().SetVexpand(false).Once()
66	mockCloseButton.EXPECT().SetHexpand(false).Once()
67	mockTitleBar.EXPECT().Append(mockCloseButton).Once()
68
69	// GestureClick is added to titleBar via AddController
70	mockTitleBar.EXPECT().AddController(mock.Anything).Once()
71
72	// Close button click handler
73	mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint(2)).Once()
74
75	// Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
76	mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
77
78	// Adding to main box - now titleBar is added directly (no button wrapper)
79	mockBox.EXPECT().Append(mockTitleBar).Once()
80	mockBox.EXPECT().Append(mockContainer).Once()
81
82	return mockTitleBar, mockFavicon, mockLabel, mockContainer
83}
84
85func TestNewStackedView_EmptyStack(t *testing.T) {
86	// Arrange
87	mockFactory, _ := setupMockFactory(t)
88
89	// Act
90	sv := layout.NewStackedView(mockFactory)
91
92	// Assert
93	require.NotNil(t, sv)
94	assert.Equal(t, 0, sv.Count())
95	assert.Equal(t, -1, sv.ActiveIndex())
96}
97
98func TestAddPane_SinglePane_BecomesActive(t *testing.T) {
99	// Arrange
100	ctx := context.Background()
101	mockFactory, mockBox := setupMockFactory(t)
102	mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
103	_ = mockFavicon
104	_ = mockLabel
105
106	// Visibility updates for active pane - titleBar is now directly in box, SetVisible called on it
107	mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
108	mockContainer.EXPECT().SetVisible(true).Once()
109	mockTitleBar.EXPECT().AddCssClass("active").Once()
110
111	sv := layout.NewStackedView(mockFactory)
112
113	// Act
114	index := sv.AddPane(ctx, "pane-1", "Test Page", "web-browser-symbolic", mockContainer)
115
116	// Assert
117	assert.Equal(t, 0, index)
118	assert.Equal(t, 1, sv.Count())
119	assert.Equal(t, 0, sv.ActiveIndex())
120}
121
122func TestAddPane_MultiplePanes_LastBecomesActive(t *testing.T) {
123	// Arrange
124	ctx := context.Background()
125	mockFactory, mockBox := setupMockFactory(t)
126
127	// First pane
128	mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
129	_ = mockFavicon1
130	_ = mockLabel1
131	mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
132	mockContainer1.EXPECT().SetVisible(true).Once()
133	mockTitleBar1.EXPECT().AddCssClass("active").Once()
134
135	// Second pane - first pane becomes inactive
136	mockTitleBar2, mockFavicon2, mockLabel2, mockContainer2 := setupPaneMocks(t, mockFactory, mockBox)
137	_ = mockFavicon2
138	_ = mockLabel2
139
140	// When second pane is added, update visibility for both
141	mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
142	mockContainer1.EXPECT().SetVisible(false).Once()
143	mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
144
145	mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
146	mockContainer2.EXPECT().SetVisible(true).Once()
147	mockTitleBar2.EXPECT().AddCssClass("active").Once()
148
149	sv := layout.NewStackedView(mockFactory)
150
151	// Act
152	sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1)
153	index := sv.AddPane(ctx, "pane-2", "Page 2", "", mockContainer2)
154
155	// Assert
156	assert.Equal(t, 1, index)
157	assert.Equal(t, 2, sv.Count())
158	assert.Equal(t, 1, sv.ActiveIndex())
159}
160
161func TestRemovePane_MiddlePane(t *testing.T) {
162	// Arrange
163	ctx := context.Background()
164	mockFactory, mockBox := setupMockFactory(t)
165
166	// Setup 3 panes (simplified - just track the key behaviors)
167	containers := make([]*mocks.MockWidget, 3)
168	titleBars := make([]*mocks.MockBoxWidget, 3)
169
170	for i := range 3 {
171		titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
172	}
173
174	// Allow any visibility calls during setup and removal
175	for i := range 3 {
176		containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
177		titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
178		titleBars[i].EXPECT().AddCssClass("active").Maybe()
179		titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
180	}
181
182	sv := layout.NewStackedView(mockFactory)
183	sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
184	sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
185	sv.AddPane(ctx, "pane-3", "Page 3", "", containers[2])
186
187	// Remove middle pane (index 1)
188	// The titleBar is now directly in the box (no button wrapper)
189	mockBox.EXPECT().Remove(titleBars[1]).Once()
190	mockBox.EXPECT().Remove(containers[1]).Once()
191
192	// Act
193	err := sv.RemovePane(ctx, 1)
194
195	// Assert
196	require.NoError(t, err)
197	assert.Equal(t, 2, sv.Count())
198}
199
200func TestRemovePane_LastPane_ReturnsError(t *testing.T) {
201	// Arrange
202	ctx := context.Background()
203	mockFactory, mockBox := setupMockFactory(t)
204	mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
205	_ = mockFavicon
206	_ = mockLabel
207
208	mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
209	mockContainer.EXPECT().SetVisible(true).Once()
210	mockTitleBar.EXPECT().AddCssClass("active").Once()
211
212	sv := layout.NewStackedView(mockFactory)
213	sv.AddPane(ctx, "pane-1", "Only Page", "", mockContainer)
214
215	// Act
216	err := sv.RemovePane(ctx, 0)
217
218	// Assert
219	require.ErrorIs(t, err, layout.ErrCannotRemoveLastPane)
220	assert.Equal(t, 1, sv.Count())
221}
222
223func TestRemovePane_EmptyStack_ReturnsError(t *testing.T) {
224	// Arrange
225	ctx := context.Background()
226	mockFactory, _ := setupMockFactory(t)
227	sv := layout.NewStackedView(mockFactory)
228
229	// Act
230	err := sv.RemovePane(ctx, 0)
231
232	// Assert
233	require.ErrorIs(t, err, layout.ErrStackEmpty)
234}
235
236func TestRemovePane_IndexOutOfBounds(t *testing.T) {
237	// Arrange
238	ctx := context.Background()
239	mockFactory, mockBox := setupMockFactory(t)
240	mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
241	_ = mockFavicon
242	_ = mockLabel
243
244	mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
245	mockContainer.EXPECT().SetVisible(true).Once()
246	mockTitleBar.EXPECT().AddCssClass("active").Once()
247
248	sv := layout.NewStackedView(mockFactory)
249	sv.AddPane(ctx, "pane-1", "Page", "", mockContainer)
250
251	// Act
252	err := sv.RemovePane(ctx, 5)
253
254	// Assert
255	require.ErrorIs(t, err, layout.ErrIndexOutOfBounds)
256}
257
258func TestSetActive_ValidIndex(t *testing.T) {
259	// Arrange
260	ctx := context.Background()
261	mockFactory, mockBox := setupMockFactory(t)
262
263	containers := make([]*mocks.MockWidget, 2)
264	titleBars := make([]*mocks.MockBoxWidget, 2)
265
266	for i := range 2 {
267		titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
268		_ = mockFavicon
269		_ = mockLabel
270
271		titleBars[i] = titleBar
272		containers[i] = container
273
274		titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
275		containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
276		titleBars[i].EXPECT().AddCssClass("active").Maybe()
277		titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
278	}
279
280	sv := layout.NewStackedView(mockFactory)
281	sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
282	sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
283
284	// Act - switch back to first pane
285	err := sv.SetActive(ctx, 0)
286
287	// Assert
288	require.NoError(t, err)
289	assert.Equal(t, 0, sv.ActiveIndex())
290}
291
292func TestSetActive_OutOfBounds(t *testing.T) {
293	// Arrange
294	ctx := context.Background()
295	mockFactory, mockBox := setupMockFactory(t)
296	mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
297	_ = mockFavicon
298	_ = mockLabel
299
300	mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
301	mockContainer.EXPECT().SetVisible(true).Once()
302	mockTitleBar.EXPECT().AddCssClass("active").Once()
303
304	sv := layout.NewStackedView(mockFactory)
305	sv.AddPane(ctx, "pane-1", "Page", "", mockContainer)
306
307	// Act
308	err := sv.SetActive(ctx, 5)
309
310	// Assert
311	require.ErrorIs(t, err, layout.ErrIndexOutOfBounds)
312}
313
314func TestSetActive_EmptyStack(t *testing.T) {
315	// Arrange
316	ctx := context.Background()
317	mockFactory, _ := setupMockFactory(t)
318	sv := layout.NewStackedView(mockFactory)
319
320	// Act
321	err := sv.SetActive(ctx, 0)
322
323	// Assert
324	require.ErrorIs(t, err, layout.ErrStackEmpty)
325}
326
327func TestUpdateTitle(t *testing.T) {
328	// Arrange
329	ctx := context.Background()
330	mockFactory, mockBox := setupMockFactory(t)
331	mockTitleBar, _, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
332
333	mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
334	mockContainer.EXPECT().SetVisible(true).Once()
335	mockTitleBar.EXPECT().AddCssClass("active").Once()
336
337	sv := layout.NewStackedView(mockFactory)
338	sv.AddPane(ctx, "pane-1", "Old Title", "", mockContainer)
339
340	mockLabel.EXPECT().SetText("New Title").Once()
341
342	// Act
343	err := sv.UpdateTitle(0, "New Title")
344
345	// Assert
346	require.NoError(t, err)
347}
348
349func TestUpdateTitle_InvalidIndex(t *testing.T) {
350	// Arrange
351	mockFactory, _ := setupMockFactory(t)
352	sv := layout.NewStackedView(mockFactory)
353
354	// Act
355	err := sv.UpdateTitle(0, "Title")
356
357	// Assert
358	require.ErrorIs(t, err, layout.ErrIndexOutOfBounds)
359}
360
361func TestUpdateFavicon(t *testing.T) {
362	// Arrange
363	ctx := context.Background()
364	mockFactory, mockBox := setupMockFactory(t)
365	mockTitleBar, mockFavicon, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
366
367	mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
368	mockContainer.EXPECT().SetVisible(true).Once()
369	mockTitleBar.EXPECT().AddCssClass("active").Once()
370
371	sv := layout.NewStackedView(mockFactory)
372	sv.AddPane(ctx, "pane-1", "Page", "", mockContainer)
373
374	mockFavicon.EXPECT().SetFromIconName("new-icon").Once()
375
376	// Act
377	err := sv.UpdateFavicon(0, "new-icon")
378
379	// Assert
380	require.NoError(t, err)
381}
382
383func TestGetContainer(t *testing.T) {
384	// Arrange
385	ctx := context.Background()
386	mockFactory, mockBox := setupMockFactory(t)
387	mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
388	_ = mockFavicon
389	_ = mockLabel
390
391	mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
392	mockContainer.EXPECT().SetVisible(true).Once()
393	mockTitleBar.EXPECT().AddCssClass("active").Once()
394
395	sv := layout.NewStackedView(mockFactory)
396	sv.AddPane(ctx, "pane-1", "Page", "", mockContainer)
397
398	// Act
399	container, err := sv.GetContainer(0)
400
401	// Assert
402	require.NoError(t, err)
403	assert.Equal(t, mockContainer, container)
404}
405
406func TestGetContainer_InvalidIndex(t *testing.T) {
407	// Arrange
408	mockFactory, _ := setupMockFactory(t)
409	sv := layout.NewStackedView(mockFactory)
410
411	// Act
412	container, err := sv.GetContainer(0)
413
414	// Assert
415	require.ErrorIs(t, err, layout.ErrIndexOutOfBounds)
416	assert.Nil(t, container)
417}
418
419func TestWidget_ReturnsBoxWidget(t *testing.T) {
420	// Arrange
421	mockFactory, mockBox := setupMockFactory(t)
422	sv := layout.NewStackedView(mockFactory)
423
424	// Act
425	widget := sv.Widget()
426
427	// Assert
428	assert.Equal(t, mockBox, widget)
429}
430
431func TestNavigateNext_WrapsAround(t *testing.T) {
432	// Arrange
433	ctx := context.Background()
434	mockFactory, mockBox := setupMockFactory(t)
435
436	containers := make([]*mocks.MockWidget, 2)
437	titleBars := make([]*mocks.MockBoxWidget, 2)
438
439	for i := range 2 {
440		titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
441		_ = mockFavicon
442		_ = mockLabel
443
444		titleBars[i] = titleBar
445		containers[i] = container
446
447		titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
448		containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
449		titleBars[i].EXPECT().AddCssClass("active").Maybe()
450		titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
451	}
452
453	sv := layout.NewStackedView(mockFactory)
454	sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
455	sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
456
457	// Currently at index 1 (last added)
458	assert.Equal(t, 1, sv.ActiveIndex())
459
460	// Act - navigate next should wrap to 0
461	err := sv.NavigateNext(ctx)
462
463	// Assert
464	require.NoError(t, err)
465	assert.Equal(t, 0, sv.ActiveIndex())
466}
467
468func TestNavigatePrevious_WrapsAround(t *testing.T) {
469	// Arrange
470	ctx := context.Background()
471	mockFactory, mockBox := setupMockFactory(t)
472
473	containers := make([]*mocks.MockWidget, 2)
474	titleBars := make([]*mocks.MockBoxWidget, 2)
475
476	for i := range 2 {
477		titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
478		_ = mockFavicon
479		_ = mockLabel
480
481		titleBars[i] = titleBar
482		containers[i] = container
483
484		titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
485		containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
486		titleBars[i].EXPECT().AddCssClass("active").Maybe()
487		titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
488	}
489
490	sv := layout.NewStackedView(mockFactory)
491	sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
492	sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
493
494	// Set to first pane
495	sv.SetActive(ctx, 0)
496
497	// Act - navigate previous should wrap to 1
498	err := sv.NavigatePrevious(ctx)
499
500	// Assert
501	require.NoError(t, err)
502	assert.Equal(t, 1, sv.ActiveIndex())
503}
504
505func TestNavigateNext_EmptyStack(t *testing.T) {
506	// Arrange
507	ctx := context.Background()
508	mockFactory, _ := setupMockFactory(t)
509	sv := layout.NewStackedView(mockFactory)
510
511	// Act
512	err := sv.NavigateNext(ctx)
513
514	// Assert
515	require.ErrorIs(t, err, layout.ErrStackEmpty)
516}
517
518func TestNavigatePrevious_EmptyStack(t *testing.T) {
519	// Arrange
520	ctx := context.Background()
521	mockFactory, _ := setupMockFactory(t)
522	sv := layout.NewStackedView(mockFactory)
523
524	// Act
525	err := sv.NavigatePrevious(ctx)
526
527	// Assert
528	require.ErrorIs(t, err, layout.ErrStackEmpty)
529}
530
531func TestSetOnActivate_Callback(t *testing.T) {
532	// Arrange
533	mockFactory, _ := setupMockFactory(t)
534	sv := layout.NewStackedView(mockFactory)
535
536	callback := func(index int) {
537		_ = index // Used to test callback registration
538	}
539
540	// Act
541	sv.SetOnActivate(callback)
542
543	// We can't easily test the callback being triggered since it requires
544	// simulating a button click. The test verifies the method doesn't panic.
545	// Integration tests would verify the actual callback invocation.
546}
547
548// setupInsertPaneMocks creates mocks needed for InsertPaneAfter with position-aware insertion.
549// Returns only the titleBar and container mocks that are needed by test assertions.
550func setupInsertPaneMocks(
551	t *testing.T,
552	mockFactory *mocks.MockWidgetFactory,
553	mockBox *mocks.MockBoxWidget,
554	siblingContainer layout.Widget,
555) (*mocks.MockBoxWidget, *mocks.MockWidget) {
556	mockTitleBar := mocks.NewMockBoxWidget(t)
557	mockFavicon := mocks.NewMockImageWidget(t)
558	mockLabel := mocks.NewMockLabelWidget(t)
559	mockCloseButton := mocks.NewMockButtonWidget(t)
560	mockContainer := mocks.NewMockWidget(t)
561
562	// Title bar creation - now directly used without button wrapper
563	mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
564	mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
565	mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
566	mockTitleBar.EXPECT().SetVexpand(false).Once()
567	mockTitleBar.EXPECT().SetHexpand(true).Once()
568
569	// Favicon
570	mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
571	mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
572	mockFavicon.EXPECT().SetPixelSize(16).Once()
573	mockTitleBar.EXPECT().Append(mockFavicon).Once()
574
575	// Label
576	mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once()
577	mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
578	mockLabel.EXPECT().SetMaxWidthChars(30).Once()
579	mockLabel.EXPECT().SetHexpand(true).Once()
580	mockLabel.EXPECT().SetXalign(float32(0.0)).Once()
581	mockTitleBar.EXPECT().Append(mockLabel).Once()
582
583	// Close button (uses SetIconName directly instead of child image)
584	mockFactory.EXPECT().NewButton().Return(mockCloseButton).Once()
585	mockCloseButton.EXPECT().SetIconName("window-close-symbolic").Once()
586	mockCloseButton.EXPECT().AddCssClass("stacked-pane-close-button").Once()
587	mockCloseButton.EXPECT().SetFocusOnClick(false).Once()
588	mockCloseButton.EXPECT().SetVexpand(false).Once()
589	mockCloseButton.EXPECT().SetHexpand(false).Once()
590	mockTitleBar.EXPECT().Append(mockCloseButton).Once()
591
592	// GestureClick is added to titleBar via AddController
593	mockTitleBar.EXPECT().AddController(mock.Anything).Once()
594
595	// Close button click handler
596	mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint(2)).Once()
597
598	// Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
599	mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
600
601	// Position-aware insertion using InsertChildAfter - now using titleBar directly
602	if siblingContainer != nil {
603		mockBox.EXPECT().InsertChildAfter(mockTitleBar, siblingContainer).Once()
604		mockBox.EXPECT().InsertChildAfter(mockContainer, mockTitleBar).Once()
605	} else {
606		// Insert at beginning using Prepend
607		mockBox.EXPECT().Prepend(mockContainer).Once()
608		mockBox.EXPECT().Prepend(mockTitleBar).Once()
609	}
610
611	return mockTitleBar, mockContainer
612}
613
614func TestInsertPaneAfter_AtBeginning(t *testing.T) {
615	// Arrange
616	ctx := context.Background()
617	mockFactory, mockBox := setupMockFactory(t)
618
619	// First pane - appended normally
620	mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
621	_ = mockFavicon1
622	_ = mockLabel1
623	mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
624	mockContainer1.EXPECT().SetVisible(true).Once()
625	mockTitleBar1.EXPECT().AddCssClass("active").Once()
626
627	sv := layout.NewStackedView(mockFactory)
628	sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1)
629
630	// Second pane - inserted at beginning (afterIndex=-1)
631	mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, nil)
632
633	// Visibility updates
634	mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
635	mockContainer1.EXPECT().SetVisible(false).Once()
636	mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
637
638	mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
639	mockContainer2.EXPECT().SetVisible(true).Once()
640	mockTitleBar2.EXPECT().AddCssClass("active").Once()
641
642	// Act
643	index := sv.InsertPaneAfter(ctx, -1, "pane-0", "Page 0", "", mockContainer2)
644
645	// Assert
646	assert.Equal(t, 0, index)
647	assert.Equal(t, 2, sv.Count())
648	assert.Equal(t, 0, sv.ActiveIndex())
649}
650
651func TestInsertPaneAfter_InMiddle(t *testing.T) {
652	// Arrange
653	ctx := context.Background()
654	mockFactory, mockBox := setupMockFactory(t)
655
656	// Setup 2 initial panes
657	containers := make([]*mocks.MockWidget, 2)
658	titleBars := make([]*mocks.MockBoxWidget, 2)
659
660	for i := range 2 {
661		titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
662		titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
663		containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
664		titleBars[i].EXPECT().AddCssClass("active").Maybe()
665		titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
666	}
667
668	sv := layout.NewStackedView(mockFactory)
669	sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
670	sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
671
672	// Set active to first pane
673	require.NoError(t, sv.SetActive(ctx, 0))
674
675	// Insert new pane after index 0 (should become index 1)
676	mockTitleBar3, mockContainer3 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[0])
677	mockTitleBar3.EXPECT().SetVisible(mock.Anything).Maybe()
678	mockContainer3.EXPECT().SetVisible(mock.Anything).Maybe()
679	mockTitleBar3.EXPECT().AddCssClass("active").Maybe()
680	mockTitleBar3.EXPECT().RemoveCssClass("active").Maybe()
681
682	// Act
683	index := sv.InsertPaneAfter(ctx, 0, "pane-1.5", "Page 1.5", "", mockContainer3)
684
685	// Assert
686	assert.Equal(t, 1, index)
687	assert.Equal(t, 3, sv.Count())
688	assert.Equal(t, 1, sv.ActiveIndex()) // New pane becomes active
689}
690
691func TestInsertPaneAfter_AtEnd(t *testing.T) {
692	// Arrange
693	ctx := context.Background()
694	mockFactory, mockBox := setupMockFactory(t)
695
696	// First pane
697	mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
698	_ = mockFavicon1
699	_ = mockLabel1
700	mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
701	mockContainer1.EXPECT().SetVisible(true).Once()
702	mockTitleBar1.EXPECT().AddCssClass("active").Once()
703
704	sv := layout.NewStackedView(mockFactory)
705	sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1)
706
707	// Insert after last pane (afterIndex=0, becomes index 1)
708	mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)
709
710	mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
711	mockContainer1.EXPECT().SetVisible(false).Once()
712	mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
713
714	mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
715	mockContainer2.EXPECT().SetVisible(true).Once()
716	mockTitleBar2.EXPECT().AddCssClass("active").Once()
717
718	// Act
719	index := sv.InsertPaneAfter(ctx, 0, "pane-2", "Page 2", "", mockContainer2)
720
721	// Assert
722	assert.Equal(t, 1, index)
723	assert.Equal(t, 2, sv.Count())
724	assert.Equal(t, 1, sv.ActiveIndex())
725}
726
727func TestInsertPaneAfter_MaintainsOrder(t *testing.T) {
728	// Arrange
729	ctx := context.Background()
730	mockFactory, mockBox := setupMockFactory(t)
731
732	// Setup 3 initial panes: A, B, C
733	containers := make([]*mocks.MockWidget, 3)
734	titleBars := make([]*mocks.MockBoxWidget, 3)
735
736	for i := range 3 {
737		titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
738		_ = mockFavicon
739		_ = mockLabel
740
741		titleBars[i] = titleBar
742		containers[i] = container
743
744		titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
745		containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
746		titleBars[i].EXPECT().AddCssClass("active").Maybe()
747		titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
748	}
749
750	sv := layout.NewStackedView(mockFactory)
751	sv.AddPane(ctx, "pane-a", "A", "", containers[0]) // index 0
752	sv.AddPane(ctx, "pane-b", "B", "", containers[1]) // index 1
753	sv.AddPane(ctx, "pane-c", "C", "", containers[2]) // index 2
754
755	// Navigate to pane B (index 1)
756	require.NoError(t, sv.SetActive(ctx, 1))
757
758	// Insert D after B (should be at index 2, C moves to index 3)
759	mockTitleBar4, mockContainer4 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[1])
760	mockTitleBar4.EXPECT().SetVisible(mock.Anything).Maybe()
761	mockContainer4.EXPECT().SetVisible(mock.Anything).Maybe()
762	mockTitleBar4.EXPECT().AddCssClass("active").Maybe()
763	mockTitleBar4.EXPECT().RemoveCssClass("active").Maybe()
764
765	// Act
766	index := sv.InsertPaneAfter(ctx, 1, "pane-d", "D", "", mockContainer4)
767
768	// Assert: Order should be A(0), B(1), D(2), C(3)
769	assert.Equal(t, 2, index)
770	assert.Equal(t, 4, sv.Count())
771	assert.Equal(t, 2, sv.ActiveIndex()) // D is now active
772
773	// Verify we can still get containers at expected indices
774	containerA, _ := sv.GetContainer(0)
775	containerB, _ := sv.GetContainer(1)
776	containerD, _ := sv.GetContainer(2)
777	containerC, _ := sv.GetContainer(3)
778
779	assert.Equal(t, containers[0], containerA)
780	assert.Equal(t, containers[1], containerB)
781	assert.Equal(t, mockContainer4, containerD)
782	assert.Equal(t, containers[2], containerC)
783}
784
785func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) {
786	// Arrange
787	ctx := context.Background()
788	mockFactory, mockBox := setupMockFactory(t)
789
790	// First pane
791	mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
792	_ = mockFavicon1
793	_ = mockLabel1
794	mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
795	mockContainer1.EXPECT().SetVisible(true).Once()
796	mockTitleBar1.EXPECT().AddCssClass("active").Once()
797
798	sv := layout.NewStackedView(mockFactory)
799	sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1)
800
801	// Try to insert at invalid index (100) - should clamp to end
802	mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)
803
804	mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
805	mockContainer1.EXPECT().SetVisible(false).Once()
806	mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
807
808	mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
809	mockContainer2.EXPECT().SetVisible(true).Once()
810	mockTitleBar2.EXPECT().AddCssClass("active").Once()
811
812	// Act - afterIndex=100 should be clamped to 0 (last valid index)
813	index := sv.InsertPaneAfter(ctx, 100, "pane-2", "Page 2", "", mockContainer2)
814
815	// Assert
816	assert.Equal(t, 1, index)
817	assert.Equal(t, 2, sv.Count())
818}

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