pane_view_test.go RawHistoryFolder

1package component_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/domain/entity"
12	"github.com/bnema/dumber/internal/ui/component"
13	"github.com/bnema/dumber/internal/ui/layout"
14	"github.com/bnema/dumber/internal/ui/layout/mocks"
15)
16
17func TestNewPaneView_CreatesOverlay(t *testing.T) {
18	// Arrange
19	mockFactory := mocks.NewMockWidgetFactory(t)
20	mockOverlay := mocks.NewMockOverlayWidget(t)
21	mockBorderBox := mocks.NewMockBoxWidget(t)
22	mockWebView := mocks.NewMockWidget(t)
23
24	mockLoadingContainer := mocks.NewMockBoxWidget(t)
25	mockLoadingContent := mocks.NewMockBoxWidget(t)
26	mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
27	mockLoadingLogo := mocks.NewMockImageWidget(t)
28
29	paneID := entity.PaneID("pane-1")
30
31	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
32	mockOverlay.EXPECT().SetHexpand(true).Once()
33	mockOverlay.EXPECT().SetVexpand(true).Once()
34	mockOverlay.EXPECT().SetVisible(true).Once()
35	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
36	mockOverlay.EXPECT().SetChild(mockWebView).Once()
37
38	setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
39
40	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
41	mockBorderBox.EXPECT().SetCanFocus(false).Once()
42	mockBorderBox.EXPECT().SetCanTarget(false).Once()
43	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
44	mockBorderBox.EXPECT().SetHexpand(true).Once()
45	mockBorderBox.EXPECT().SetVexpand(true).Once()
46	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
47	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
48	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
49
50	// Act
51	pv := component.NewPaneView(context.Background(), mockFactory, paneID, mockWebView)
52
53	// Assert
54	require.NotNil(t, pv)
55	assert.Equal(t, paneID, pv.PaneID())
56	assert.Equal(t, mockWebView, pv.WebViewWidget())
57}
58
59func TestNewPaneView_NilWebView(t *testing.T) {
60	// Arrange
61	mockFactory := mocks.NewMockWidgetFactory(t)
62	mockOverlay := mocks.NewMockOverlayWidget(t)
63	mockBorderBox := mocks.NewMockBoxWidget(t)
64
65	mockLoadingContainer := mocks.NewMockBoxWidget(t)
66	mockLoadingContent := mocks.NewMockBoxWidget(t)
67	mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
68	mockLoadingLogo := mocks.NewMockImageWidget(t)
69
70	paneID := entity.PaneID("pane-1")
71
72	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
73	mockOverlay.EXPECT().SetHexpand(true).Once()
74	mockOverlay.EXPECT().SetVexpand(true).Once()
75	mockOverlay.EXPECT().SetVisible(true).Once()
76	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
77	// SetChild should NOT be called when webview is nil
78
79	setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
80
81	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
82	mockBorderBox.EXPECT().SetCanFocus(false).Once()
83	mockBorderBox.EXPECT().SetCanTarget(false).Once()
84	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
85	mockBorderBox.EXPECT().SetHexpand(true).Once()
86	mockBorderBox.EXPECT().SetVexpand(true).Once()
87	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
88	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
89	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
90
91	// Act
92	pv := component.NewPaneView(context.Background(), mockFactory, paneID, nil)
93
94	// Assert
95	require.NotNil(t, pv)
96	assert.Nil(t, pv.WebViewWidget())
97}
98
99func TestSetActive_True_AddsCSSClass(t *testing.T) {
100	// Arrange
101	mockFactory := mocks.NewMockWidgetFactory(t)
102	mockOverlay := mocks.NewMockOverlayWidget(t)
103	mockBorderBox := mocks.NewMockBoxWidget(t)
104	mockWebView := mocks.NewMockWidget(t)
105
106	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
107
108	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
109
110	// Expect CSS class to be added
111	mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
112
113	// Act
114	pv.SetActive(true)
115
116	// Assert
117	assert.True(t, pv.IsActive())
118}
119
120func TestSetActive_False_RemovesCSSClass(t *testing.T) {
121	// Arrange
122	mockFactory := mocks.NewMockWidgetFactory(t)
123	mockOverlay := mocks.NewMockOverlayWidget(t)
124	mockBorderBox := mocks.NewMockBoxWidget(t)
125	mockWebView := mocks.NewMockWidget(t)
126
127	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
128
129	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
130
131	// First activate
132	mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
133	pv.SetActive(true)
134
135	// Then deactivate
136	mockBorderBox.EXPECT().RemoveCssClass("pane-active").Once()
137
138	// Act
139	pv.SetActive(false)
140
141	// Assert
142	assert.False(t, pv.IsActive())
143}
144
145func TestSetActive_NoChangeWhenSameState(t *testing.T) {
146	// Arrange
147	mockFactory := mocks.NewMockWidgetFactory(t)
148	mockOverlay := mocks.NewMockOverlayWidget(t)
149	mockBorderBox := mocks.NewMockBoxWidget(t)
150	mockWebView := mocks.NewMockWidget(t)
151
152	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
153
154	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
155
156	// Act - setting false when already false should not call RemoveCssClass
157	pv.SetActive(false)
158
159	// Assert - no mock expectations for CSS changes
160	assert.False(t, pv.IsActive())
161}
162
163func TestPaneID_ReturnsPaneID(t *testing.T) {
164	// Arrange
165	mockFactory := mocks.NewMockWidgetFactory(t)
166	mockOverlay := mocks.NewMockOverlayWidget(t)
167	mockBorderBox := mocks.NewMockBoxWidget(t)
168	mockWebView := mocks.NewMockWidget(t)
169
170	paneID := entity.PaneID("test-pane-123")
171	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
172
173	pv := component.NewPaneView(context.Background(), mockFactory, paneID, mockWebView)
174
175	// Act
176	result := pv.PaneID()
177
178	// Assert
179	assert.Equal(t, paneID, result)
180}
181
182func TestWebViewWidget_ReturnsWebView(t *testing.T) {
183	// Arrange
184	mockFactory := mocks.NewMockWidgetFactory(t)
185	mockOverlay := mocks.NewMockOverlayWidget(t)
186	mockBorderBox := mocks.NewMockBoxWidget(t)
187	mockWebView := mocks.NewMockWidget(t)
188
189	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
190
191	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
192
193	// Act
194	result := pv.WebViewWidget()
195
196	// Assert
197	assert.Equal(t, mockWebView, result)
198}
199
200func TestSetWebViewWidget_ReplacesWidget(t *testing.T) {
201	// Arrange
202	mockFactory := mocks.NewMockWidgetFactory(t)
203	mockOverlay := mocks.NewMockOverlayWidget(t)
204	mockBorderBox := mocks.NewMockBoxWidget(t)
205	mockOldWebView := mocks.NewMockWidget(t)
206	mockNewWebView := mocks.NewMockWidget(t)
207
208	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockOldWebView)
209
210	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockOldWebView)
211
212	// Expect removal of old widget and addition of new
213	mockOverlay.EXPECT().SetChild(nil).Once()
214	mockOverlay.EXPECT().GetAllocatedWidth().Return(0).Twice()
215	mockOverlay.EXPECT().GetAllocatedHeight().Return(0).Twice()
216	mockNewWebView.EXPECT().GetParent().Return(nil).Once()
217	mockNewWebView.EXPECT().IsVisible().Return(false).Once()
218	mockNewWebView.EXPECT().GetAllocatedWidth().Return(0).Twice()
219	mockNewWebView.EXPECT().GetAllocatedHeight().Return(0).Twice()
220	mockOverlay.EXPECT().SetChild(mockNewWebView).Once()
221
222	// Act
223	pv.SetWebViewWidget(mockNewWebView)
224
225	// Assert
226	assert.Equal(t, mockNewWebView, pv.WebViewWidget())
227}
228
229func TestSetWebViewWidget_ReparentsWidgetWithExistingParent(t *testing.T) {
230	// Arrange
231	mockFactory := mocks.NewMockWidgetFactory(t)
232	mockOverlay := mocks.NewMockOverlayWidget(t)
233	mockBorderBox := mocks.NewMockBoxWidget(t)
234	mockOldWebView := mocks.NewMockWidget(t)
235	mockNewWebView := mocks.NewMockWidget(t)
236	mockParent := mocks.NewMockWidget(t)
237
238	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockOldWebView)
239
240	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockOldWebView)
241
242	mockOverlay.EXPECT().SetChild(nil).Once()
243	mockOverlay.EXPECT().GetAllocatedWidth().Return(0).Twice()
244	mockOverlay.EXPECT().GetAllocatedHeight().Return(0).Twice()
245	mockNewWebView.EXPECT().GetAllocatedWidth().Return(0).Twice()
246	mockNewWebView.EXPECT().GetAllocatedHeight().Return(0).Twice()
247	mockNewWebView.EXPECT().GetParent().Return(mockParent).Once()
248	mockNewWebView.EXPECT().IsVisible().Return(true).Once()
249	mockNewWebView.EXPECT().Unparent().Once()
250	mockOverlay.EXPECT().SetChild(mockNewWebView).Once()
251	mockNewWebView.EXPECT().SetVisible(true).Once()
252
253	// Loading skeleton is hidden immediately for a reparented, already-visible widget.
254
255	// Act
256	pv.SetWebViewWidget(mockNewWebView)
257
258	// Assert
259	assert.Equal(t, mockNewWebView, pv.WebViewWidget())
260}
261
262func TestSetWebViewWidget_FromNil(t *testing.T) {
263	// Arrange
264	mockFactory := mocks.NewMockWidgetFactory(t)
265	mockOverlay := mocks.NewMockOverlayWidget(t)
266	mockBorderBox := mocks.NewMockBoxWidget(t)
267	mockNewWebView := mocks.NewMockWidget(t)
268
269	setupPaneViewMocksNoWebView(t, mockFactory, mockOverlay, mockBorderBox)
270
271	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), nil)
272
273	// Expect only setting new child (no removal since old was nil)
274	mockOverlay.EXPECT().GetAllocatedWidth().Return(0).Twice()
275	mockOverlay.EXPECT().GetAllocatedHeight().Return(0).Twice()
276	mockNewWebView.EXPECT().GetParent().Return(nil).Once()
277	mockNewWebView.EXPECT().IsVisible().Return(false).Once()
278	mockNewWebView.EXPECT().GetAllocatedWidth().Return(0).Twice()
279	mockNewWebView.EXPECT().GetAllocatedHeight().Return(0).Twice()
280	mockOverlay.EXPECT().SetChild(mockNewWebView).Once()
281
282	// Act
283	pv.SetWebViewWidget(mockNewWebView)
284
285	// Assert
286	assert.Equal(t, mockNewWebView, pv.WebViewWidget())
287}
288
289func TestGrabFocus_DelegatesToWebView(t *testing.T) {
290	// Arrange
291	mockFactory := mocks.NewMockWidgetFactory(t)
292	mockOverlay := mocks.NewMockOverlayWidget(t)
293	mockBorderBox := mocks.NewMockBoxWidget(t)
294	mockWebView := mocks.NewMockWidget(t)
295
296	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
297
298	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
299
300	mockWebView.EXPECT().GrabFocus().Return(true).Once()
301
302	// Act
303	result := pv.GrabFocus()
304
305	// Assert
306	assert.True(t, result)
307}
308
309func TestGrabFocus_NilWebView_ReturnsFalse(t *testing.T) {
310	// Arrange
311	mockFactory := mocks.NewMockWidgetFactory(t)
312	mockOverlay := mocks.NewMockOverlayWidget(t)
313	mockBorderBox := mocks.NewMockBoxWidget(t)
314
315	setupPaneViewMocksNoWebView(t, mockFactory, mockOverlay, mockBorderBox)
316
317	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), nil)
318
319	// Act
320	result := pv.GrabFocus()
321
322	// Assert
323	assert.False(t, result)
324}
325
326func TestHasFocus_DelegatesToWebView(t *testing.T) {
327	// Arrange
328	mockFactory := mocks.NewMockWidgetFactory(t)
329	mockOverlay := mocks.NewMockOverlayWidget(t)
330	mockBorderBox := mocks.NewMockBoxWidget(t)
331	mockWebView := mocks.NewMockWidget(t)
332
333	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
334
335	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
336
337	mockWebView.EXPECT().HasFocus().Return(true).Once()
338
339	// Act
340	result := pv.HasFocus()
341
342	// Assert
343	assert.True(t, result)
344}
345
346func TestHasFocus_NilWebView_ReturnsFalse(t *testing.T) {
347	// Arrange
348	mockFactory := mocks.NewMockWidgetFactory(t)
349	mockOverlay := mocks.NewMockOverlayWidget(t)
350	mockBorderBox := mocks.NewMockBoxWidget(t)
351
352	setupPaneViewMocksNoWebView(t, mockFactory, mockOverlay, mockBorderBox)
353
354	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), nil)
355
356	// Act
357	result := pv.HasFocus()
358
359	// Assert
360	assert.False(t, result)
361}
362
363func TestWidget_ReturnsOverlay(t *testing.T) {
364	// Arrange
365	mockFactory := mocks.NewMockWidgetFactory(t)
366	mockOverlay := mocks.NewMockOverlayWidget(t)
367	mockBorderBox := mocks.NewMockBoxWidget(t)
368	mockWebView := mocks.NewMockWidget(t)
369
370	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
371
372	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
373
374	// Act
375	result := pv.Widget()
376
377	// Assert
378	assert.Equal(t, mockOverlay, result)
379}
380
381func TestOverlay_ReturnsOverlayWidget(t *testing.T) {
382	// Arrange
383	mockFactory := mocks.NewMockWidgetFactory(t)
384	mockOverlay := mocks.NewMockOverlayWidget(t)
385	mockBorderBox := mocks.NewMockBoxWidget(t)
386	mockWebView := mocks.NewMockWidget(t)
387
388	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
389
390	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
391
392	// Act
393	result := pv.Overlay()
394
395	// Assert
396	assert.Equal(t, mockOverlay, result)
397}
398
399func TestShow_DelegatesToOverlay(t *testing.T) {
400	// Arrange
401	mockFactory := mocks.NewMockWidgetFactory(t)
402	mockOverlay := mocks.NewMockOverlayWidget(t)
403	mockBorderBox := mocks.NewMockBoxWidget(t)
404	mockWebView := mocks.NewMockWidget(t)
405
406	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
407
408	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
409
410	mockOverlay.EXPECT().Show().Once()
411
412	// Act
413	pv.Show()
414
415	// Assert - verified by mock expectations
416}
417
418func TestHide_DelegatesToOverlay(t *testing.T) {
419	// Arrange
420	mockFactory := mocks.NewMockWidgetFactory(t)
421	mockOverlay := mocks.NewMockOverlayWidget(t)
422	mockBorderBox := mocks.NewMockBoxWidget(t)
423	mockWebView := mocks.NewMockWidget(t)
424
425	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
426
427	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
428
429	mockOverlay.EXPECT().Hide().Once()
430
431	// Act
432	pv.Hide()
433
434	// Assert - verified by mock expectations
435}
436
437func TestSetVisible_DelegatesToOverlay(t *testing.T) {
438	// Arrange
439	mockFactory := mocks.NewMockWidgetFactory(t)
440	mockOverlay := mocks.NewMockOverlayWidget(t)
441	mockBorderBox := mocks.NewMockBoxWidget(t)
442	mockWebView := mocks.NewMockWidget(t)
443
444	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
445
446	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
447
448	mockOverlay.EXPECT().SetVisible(true).Once()
449
450	// Act
451	pv.SetVisible(true)
452
453	// Assert - verified by mock expectations
454}
455
456func TestIsVisible_DelegatesToOverlay(t *testing.T) {
457	// Arrange
458	mockFactory := mocks.NewMockWidgetFactory(t)
459	mockOverlay := mocks.NewMockOverlayWidget(t)
460	mockBorderBox := mocks.NewMockBoxWidget(t)
461	mockWebView := mocks.NewMockWidget(t)
462
463	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
464
465	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
466
467	mockOverlay.EXPECT().IsVisible().Return(true).Once()
468
469	// Act
470	result := pv.IsVisible()
471
472	// Assert
473	assert.True(t, result)
474}
475
476func TestAddCssClass_DelegatesToOverlay(t *testing.T) {
477	// Arrange
478	mockFactory := mocks.NewMockWidgetFactory(t)
479	mockOverlay := mocks.NewMockOverlayWidget(t)
480	mockBorderBox := mocks.NewMockBoxWidget(t)
481	mockWebView := mocks.NewMockWidget(t)
482
483	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
484
485	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
486
487	mockOverlay.EXPECT().AddCssClass("custom-class").Once()
488
489	// Act
490	pv.AddCssClass("custom-class")
491
492	// Assert - verified by mock expectations
493}
494
495func TestRemoveCssClass_DelegatesToOverlay(t *testing.T) {
496	// Arrange
497	mockFactory := mocks.NewMockWidgetFactory(t)
498	mockOverlay := mocks.NewMockOverlayWidget(t)
499	mockBorderBox := mocks.NewMockBoxWidget(t)
500	mockWebView := mocks.NewMockWidget(t)
501
502	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
503
504	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
505
506	mockOverlay.EXPECT().RemoveCssClass("custom-class").Once()
507
508	// Act
509	pv.RemoveCssClass("custom-class")
510
511	// Assert - verified by mock expectations
512}
513
514func TestSetOnFocusIn_SetsCallback(t *testing.T) {
515	// Arrange
516	mockFactory := mocks.NewMockWidgetFactory(t)
517	mockOverlay := mocks.NewMockOverlayWidget(t)
518	mockBorderBox := mocks.NewMockBoxWidget(t)
519	mockWebView := mocks.NewMockWidget(t)
520
521	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
522
523	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
524
525	callback := func(paneID entity.PaneID) {}
526
527	// Act
528	pv.SetOnFocusIn(callback)
529
530	// Assert - callback is set (we can't directly verify without triggering focus)
531	assert.NotNil(t, pv)
532}
533
534func TestSetOnFocusOut_SetsCallback(t *testing.T) {
535	// Arrange
536	mockFactory := mocks.NewMockWidgetFactory(t)
537	mockOverlay := mocks.NewMockOverlayWidget(t)
538	mockBorderBox := mocks.NewMockBoxWidget(t)
539	mockWebView := mocks.NewMockWidget(t)
540
541	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
542
543	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
544
545	callback := func(paneID entity.PaneID) {}
546
547	// Act
548	pv.SetOnFocusOut(callback)
549
550	// Assert - callback is set (we can't directly verify without triggering focus)
551	assert.NotNil(t, pv)
552}
553
554// Helper function to setup common mock expectations for PaneView creation
555func setupLoadingSkeletonMocks(
556	t *testing.T,
557	mockFactory *mocks.MockWidgetFactory,
558	mockOverlay *mocks.MockOverlayWidget,
559	mockLoadingContainer *mocks.MockBoxWidget,
560	mockLoadingContent *mocks.MockBoxWidget,
561	mockLoadingSpinner *mocks.MockSpinnerWidget,
562	mockLoadingLogo *mocks.MockImageWidget,
563) {
564	mockLoadingVersion := mocks.NewMockLabelWidget(t)
565
566	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
567	mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
568	mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()
569	mockLoadingContainer.EXPECT().SetHalign(mock.Anything).Maybe()
570	mockLoadingContainer.EXPECT().SetValign(mock.Anything).Maybe()
571	mockLoadingContainer.EXPECT().SetCanFocus(false).Maybe()
572	mockLoadingContainer.EXPECT().SetCanTarget(false).Maybe()
573	mockLoadingContainer.EXPECT().AddCssClass("loading-skeleton").Maybe()
574	mockLoadingContainer.EXPECT().SetVisible(mock.Anything).Maybe()
575
576	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 6).Return(mockLoadingContent).Once()
577	mockLoadingContent.EXPECT().SetHalign(mock.Anything).Maybe()
578	mockLoadingContent.EXPECT().SetValign(mock.Anything).Maybe()
579	mockLoadingContent.EXPECT().SetCanFocus(false).Maybe()
580	mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()
581	mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()
582
583	mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
584	mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
585	mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
586	mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
587	mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
588	mockLoadingLogo.EXPECT().SetSizeRequest(256, 256).Maybe()
589	mockLoadingLogo.EXPECT().SetPixelSize(256).Maybe()
590	mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
591	mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
592
593	mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()
594	mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()
595	mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()
596	mockLoadingSpinner.EXPECT().SetCanFocus(false).Maybe()
597	mockLoadingSpinner.EXPECT().SetCanTarget(false).Maybe()
598	mockLoadingSpinner.EXPECT().SetSizeRequest(32, 32).Maybe()
599	mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
600	mockLoadingSpinner.EXPECT().Start().Maybe()
601	mockLoadingSpinner.EXPECT().Stop().Maybe()
602
603	mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLoadingVersion).Once()
604	mockLoadingVersion.EXPECT().SetHalign(mock.Anything).Maybe()
605	mockLoadingVersion.EXPECT().SetValign(mock.Anything).Maybe()
606	mockLoadingVersion.EXPECT().SetCanFocus(false).Maybe()
607	mockLoadingVersion.EXPECT().SetCanTarget(false).Maybe()
608	mockLoadingVersion.EXPECT().SetMaxWidthChars(mock.Anything).Maybe()
609	mockLoadingVersion.EXPECT().SetEllipsize(mock.Anything).Maybe()
610	mockLoadingVersion.EXPECT().AddCssClass("loading-skeleton-version").Maybe()
611
612	mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
613	mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
614	mockLoadingContent.EXPECT().Append(mockLoadingVersion).Maybe()
615	mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
616
617	mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
618	mockOverlay.EXPECT().SetClipOverlay(mockLoadingContainer, false).Once()
619	mockOverlay.EXPECT().SetMeasureOverlay(mockLoadingContainer, false).Once()
620}
621
622// Helper function to setup common mock expectations for PaneView creation
623func setupPaneViewMocks(
624	t *testing.T,
625	mockFactory *mocks.MockWidgetFactory,
626	mockOverlay *mocks.MockOverlayWidget,
627	mockBorderBox *mocks.MockBoxWidget,
628	mockWebView *mocks.MockWidget,
629) {
630	mockLoadingContainer := mocks.NewMockBoxWidget(t)
631	mockLoadingContent := mocks.NewMockBoxWidget(t)
632	mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
633	mockLoadingLogo := mocks.NewMockImageWidget(t)
634
635	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
636	mockOverlay.EXPECT().SetHexpand(true).Once()
637	mockOverlay.EXPECT().SetVexpand(true).Once()
638	mockOverlay.EXPECT().SetVisible(true).Once()
639	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
640	mockOverlay.EXPECT().SetChild(mockWebView).Once()
641
642	setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
643
644	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
645	mockBorderBox.EXPECT().SetCanFocus(false).Once()
646	mockBorderBox.EXPECT().SetCanTarget(false).Once()
647	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
648	mockBorderBox.EXPECT().SetHexpand(true).Once()
649	mockBorderBox.EXPECT().SetVexpand(true).Once()
650	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
651	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
652	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
653}
654
655func setupPaneViewMocksNoWebView(
656	t *testing.T,
657	mockFactory *mocks.MockWidgetFactory,
658	mockOverlay *mocks.MockOverlayWidget,
659	mockBorderBox *mocks.MockBoxWidget,
660) {
661	mockLoadingContainer := mocks.NewMockBoxWidget(t)
662	mockLoadingContent := mocks.NewMockBoxWidget(t)
663	mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
664	mockLoadingLogo := mocks.NewMockImageWidget(t)
665
666	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
667	mockOverlay.EXPECT().SetHexpand(true).Once()
668	mockOverlay.EXPECT().SetVexpand(true).Once()
669	mockOverlay.EXPECT().SetVisible(true).Once()
670	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
671
672	setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
673
674	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
675	mockBorderBox.EXPECT().SetCanFocus(false).Once()
676	mockBorderBox.EXPECT().SetCanTarget(false).Once()
677	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
678	mockBorderBox.EXPECT().SetHexpand(true).Once()
679	mockBorderBox.EXPECT().SetVexpand(true).Once()
680	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
681	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
682	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
683}
684
685func TestSetVimMode_TrueAddsPaneAccentWithoutCreatingLabel(t *testing.T) {
686	mockFactory := mocks.NewMockWidgetFactory(t)
687	mockOverlay := mocks.NewMockOverlayWidget(t)
688	mockBorderBox := mocks.NewMockBoxWidget(t)
689	mockWebView := mocks.NewMockWidget(t)
690	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
691	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
692
693	pv.SetVimMode(true)
694
695	require.True(t, pv.IsVimMode())
696}
697
698func TestSetVimMode_FalseRemovesPaneAccent(t *testing.T) {
699	mockFactory := mocks.NewMockWidgetFactory(t)
700	mockOverlay := mocks.NewMockOverlayWidget(t)
701	mockBorderBox := mocks.NewMockBoxWidget(t)
702	mockWebView := mocks.NewMockWidget(t)
703	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
704	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
705
706	pv.SetVimMode(true)
707	pv.SetVimMode(false)
708
709	require.False(t, pv.IsVimMode())
710}
711
712func TestSetVimMode_NoChangeWhenSameState(t *testing.T) {
713	mockFactory := mocks.NewMockWidgetFactory(t)
714	mockOverlay := mocks.NewMockOverlayWidget(t)
715	mockBorderBox := mocks.NewMockBoxWidget(t)
716	mockWebView := mocks.NewMockWidget(t)
717	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
718	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
719
720	pv.SetVimMode(false)
721
722	require.False(t, pv.IsVimMode())
723}
724
725func TestSetVimMode_TrueThenTrueIsIdempotent(t *testing.T) {
726	mockFactory := mocks.NewMockWidgetFactory(t)
727	mockOverlay := mocks.NewMockOverlayWidget(t)
728	mockBorderBox := mocks.NewMockBoxWidget(t)
729	mockWebView := mocks.NewMockWidget(t)
730	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
731	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
732
733	pv.SetVimMode(true)
734	pv.SetVimMode(true)
735
736	require.True(t, pv.IsVimMode())
737}
738
739func TestVimMode_CleanupHasNoIndicatorOverlay(t *testing.T) {
740	mockFactory := mocks.NewMockWidgetFactory(t)
741	mockOverlay := mocks.NewMockOverlayWidget(t)
742	mockBorderBox := mocks.NewMockBoxWidget(t)
743	mockWebView := mocks.NewMockWidget(t)
744	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
745	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
746
747	pv.SetVimMode(true)
748	mockOverlay.EXPECT().RemoveOverlay(mock.Anything).Once()
749	mockOverlay.EXPECT().SetChild(nil).Once()
750
751	pv.Cleanup()
752}
753
754func TestVimMode_NewPaneViewInactiveByDefault(t *testing.T) {
755	mockFactory := mocks.NewMockWidgetFactory(t)
756	mockOverlay := mocks.NewMockOverlayWidget(t)
757	mockBorderBox := mocks.NewMockBoxWidget(t)
758	mockWebView := mocks.NewMockWidget(t)
759	setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
760
761	pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
762
763	require.False(t, pv.IsVimMode())
764}

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