v0.20.1 d9a391f

pane_view_test.go RawHistoryFolder

1package component_test
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/assert"
7	"github.com/stretchr/testify/require"
8
9	"github.com/bnema/dumber/internal/domain/entity"
10	"github.com/bnema/dumber/internal/ui/component"
11	"github.com/bnema/dumber/internal/ui/layout"
12	"github.com/bnema/dumber/internal/ui/layout/mocks"
13)
14
15func TestNewPaneView_CreatesOverlay(t *testing.T) {
16	// Arrange
17	mockFactory := mocks.NewMockWidgetFactory(t)
18	mockOverlay := mocks.NewMockOverlayWidget(t)
19	mockBorderBox := mocks.NewMockBoxWidget(t)
20	mockWebView := mocks.NewMockWidget(t)
21
22	paneID := entity.PaneID("pane-1")
23
24	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
25	mockOverlay.EXPECT().SetHexpand(true).Once()
26	mockOverlay.EXPECT().SetVexpand(true).Once()
27	mockOverlay.EXPECT().SetVisible(true).Once()
28	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
29	mockOverlay.EXPECT().SetChild(mockWebView).Once()
30
31	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
32	mockBorderBox.EXPECT().SetCanFocus(false).Once()
33	mockBorderBox.EXPECT().SetCanTarget(false).Once()
34	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
35	mockBorderBox.EXPECT().SetHexpand(true).Once()
36	mockBorderBox.EXPECT().SetVexpand(true).Once()
37	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
38	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
39	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
40
41	// Act
42	pv := component.NewPaneView(mockFactory, paneID, mockWebView)
43
44	// Assert
45	require.NotNil(t, pv)
46	assert.Equal(t, paneID, pv.PaneID())
47	assert.Equal(t, mockWebView, pv.WebViewWidget())
48}
49
50func TestNewPaneView_NilWebView(t *testing.T) {
51	// Arrange
52	mockFactory := mocks.NewMockWidgetFactory(t)
53	mockOverlay := mocks.NewMockOverlayWidget(t)
54	mockBorderBox := mocks.NewMockBoxWidget(t)
55
56	paneID := entity.PaneID("pane-1")
57
58	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
59	mockOverlay.EXPECT().SetHexpand(true).Once()
60	mockOverlay.EXPECT().SetVexpand(true).Once()
61	mockOverlay.EXPECT().SetVisible(true).Once()
62	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
63	// SetChild should NOT be called when webview is nil
64
65	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
66	mockBorderBox.EXPECT().SetCanFocus(false).Once()
67	mockBorderBox.EXPECT().SetCanTarget(false).Once()
68	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
69	mockBorderBox.EXPECT().SetHexpand(true).Once()
70	mockBorderBox.EXPECT().SetVexpand(true).Once()
71	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
72	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
73	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
74
75	// Act
76	pv := component.NewPaneView(mockFactory, paneID, nil)
77
78	// Assert
79	require.NotNil(t, pv)
80	assert.Nil(t, pv.WebViewWidget())
81}
82
83func TestSetActive_True_AddsCSSClass(t *testing.T) {
84	// Arrange
85	mockFactory := mocks.NewMockWidgetFactory(t)
86	mockOverlay := mocks.NewMockOverlayWidget(t)
87	mockBorderBox := mocks.NewMockBoxWidget(t)
88	mockWebView := mocks.NewMockWidget(t)
89
90	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
91
92	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
93
94	// Expect CSS class to be added
95	mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
96
97	// Act
98	pv.SetActive(true)
99
100	// Assert
101	assert.True(t, pv.IsActive())
102}
103
104func TestSetActive_False_RemovesCSSClass(t *testing.T) {
105	// Arrange
106	mockFactory := mocks.NewMockWidgetFactory(t)
107	mockOverlay := mocks.NewMockOverlayWidget(t)
108	mockBorderBox := mocks.NewMockBoxWidget(t)
109	mockWebView := mocks.NewMockWidget(t)
110
111	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
112
113	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
114
115	// First activate
116	mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
117	pv.SetActive(true)
118
119	// Then deactivate
120	mockBorderBox.EXPECT().RemoveCssClass("pane-active").Once()
121
122	// Act
123	pv.SetActive(false)
124
125	// Assert
126	assert.False(t, pv.IsActive())
127}
128
129func TestSetActive_NoChangeWhenSameState(t *testing.T) {
130	// Arrange
131	mockFactory := mocks.NewMockWidgetFactory(t)
132	mockOverlay := mocks.NewMockOverlayWidget(t)
133	mockBorderBox := mocks.NewMockBoxWidget(t)
134	mockWebView := mocks.NewMockWidget(t)
135
136	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
137
138	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
139
140	// Act - setting false when already false should not call RemoveCssClass
141	pv.SetActive(false)
142
143	// Assert - no mock expectations for CSS changes
144	assert.False(t, pv.IsActive())
145}
146
147func TestPaneID_ReturnsPaneID(t *testing.T) {
148	// Arrange
149	mockFactory := mocks.NewMockWidgetFactory(t)
150	mockOverlay := mocks.NewMockOverlayWidget(t)
151	mockBorderBox := mocks.NewMockBoxWidget(t)
152	mockWebView := mocks.NewMockWidget(t)
153
154	paneID := entity.PaneID("test-pane-123")
155	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
156
157	pv := component.NewPaneView(mockFactory, paneID, mockWebView)
158
159	// Act
160	result := pv.PaneID()
161
162	// Assert
163	assert.Equal(t, paneID, result)
164}
165
166func TestWebViewWidget_ReturnsWebView(t *testing.T) {
167	// Arrange
168	mockFactory := mocks.NewMockWidgetFactory(t)
169	mockOverlay := mocks.NewMockOverlayWidget(t)
170	mockBorderBox := mocks.NewMockBoxWidget(t)
171	mockWebView := mocks.NewMockWidget(t)
172
173	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
174
175	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
176
177	// Act
178	result := pv.WebViewWidget()
179
180	// Assert
181	assert.Equal(t, mockWebView, result)
182}
183
184func TestSetWebViewWidget_ReplacesWidget(t *testing.T) {
185	// Arrange
186	mockFactory := mocks.NewMockWidgetFactory(t)
187	mockOverlay := mocks.NewMockOverlayWidget(t)
188	mockBorderBox := mocks.NewMockBoxWidget(t)
189	mockOldWebView := mocks.NewMockWidget(t)
190	mockNewWebView := mocks.NewMockWidget(t)
191
192	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockOldWebView)
193
194	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockOldWebView)
195
196	// Expect removal of old widget and addition of new
197	mockOverlay.EXPECT().SetChild(nil).Once()
198	mockNewWebView.EXPECT().GetParent().Return(nil).Once()
199	mockNewWebView.EXPECT().SetVisible(true).Once()
200	mockOverlay.EXPECT().SetChild(mockNewWebView).Once()
201
202	// Act
203	pv.SetWebViewWidget(mockNewWebView)
204
205	// Assert
206	assert.Equal(t, mockNewWebView, pv.WebViewWidget())
207}
208
209func TestSetWebViewWidget_FromNil(t *testing.T) {
210	// Arrange
211	mockFactory := mocks.NewMockWidgetFactory(t)
212	mockOverlay := mocks.NewMockOverlayWidget(t)
213	mockBorderBox := mocks.NewMockBoxWidget(t)
214	mockNewWebView := mocks.NewMockWidget(t)
215
216	setupPaneViewMocksNoWebView(mockFactory, mockOverlay, mockBorderBox)
217
218	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), nil)
219
220	// Expect only setting new child (no removal since old was nil)
221	mockNewWebView.EXPECT().GetParent().Return(nil).Once()
222	mockNewWebView.EXPECT().SetVisible(true).Once()
223	mockOverlay.EXPECT().SetChild(mockNewWebView).Once()
224
225	// Act
226	pv.SetWebViewWidget(mockNewWebView)
227
228	// Assert
229	assert.Equal(t, mockNewWebView, pv.WebViewWidget())
230}
231
232func TestGrabFocus_DelegatesToWebView(t *testing.T) {
233	// Arrange
234	mockFactory := mocks.NewMockWidgetFactory(t)
235	mockOverlay := mocks.NewMockOverlayWidget(t)
236	mockBorderBox := mocks.NewMockBoxWidget(t)
237	mockWebView := mocks.NewMockWidget(t)
238
239	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
240
241	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
242
243	mockWebView.EXPECT().GrabFocus().Return(true).Once()
244
245	// Act
246	result := pv.GrabFocus()
247
248	// Assert
249	assert.True(t, result)
250}
251
252func TestGrabFocus_NilWebView_ReturnsFalse(t *testing.T) {
253	// Arrange
254	mockFactory := mocks.NewMockWidgetFactory(t)
255	mockOverlay := mocks.NewMockOverlayWidget(t)
256	mockBorderBox := mocks.NewMockBoxWidget(t)
257
258	setupPaneViewMocksNoWebView(mockFactory, mockOverlay, mockBorderBox)
259
260	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), nil)
261
262	// Act
263	result := pv.GrabFocus()
264
265	// Assert
266	assert.False(t, result)
267}
268
269func TestHasFocus_DelegatesToWebView(t *testing.T) {
270	// Arrange
271	mockFactory := mocks.NewMockWidgetFactory(t)
272	mockOverlay := mocks.NewMockOverlayWidget(t)
273	mockBorderBox := mocks.NewMockBoxWidget(t)
274	mockWebView := mocks.NewMockWidget(t)
275
276	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
277
278	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
279
280	mockWebView.EXPECT().HasFocus().Return(true).Once()
281
282	// Act
283	result := pv.HasFocus()
284
285	// Assert
286	assert.True(t, result)
287}
288
289func TestHasFocus_NilWebView_ReturnsFalse(t *testing.T) {
290	// Arrange
291	mockFactory := mocks.NewMockWidgetFactory(t)
292	mockOverlay := mocks.NewMockOverlayWidget(t)
293	mockBorderBox := mocks.NewMockBoxWidget(t)
294
295	setupPaneViewMocksNoWebView(mockFactory, mockOverlay, mockBorderBox)
296
297	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), nil)
298
299	// Act
300	result := pv.HasFocus()
301
302	// Assert
303	assert.False(t, result)
304}
305
306func TestWidget_ReturnsOverlay(t *testing.T) {
307	// Arrange
308	mockFactory := mocks.NewMockWidgetFactory(t)
309	mockOverlay := mocks.NewMockOverlayWidget(t)
310	mockBorderBox := mocks.NewMockBoxWidget(t)
311	mockWebView := mocks.NewMockWidget(t)
312
313	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
314
315	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
316
317	// Act
318	result := pv.Widget()
319
320	// Assert
321	assert.Equal(t, mockOverlay, result)
322}
323
324func TestOverlay_ReturnsOverlayWidget(t *testing.T) {
325	// Arrange
326	mockFactory := mocks.NewMockWidgetFactory(t)
327	mockOverlay := mocks.NewMockOverlayWidget(t)
328	mockBorderBox := mocks.NewMockBoxWidget(t)
329	mockWebView := mocks.NewMockWidget(t)
330
331	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
332
333	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
334
335	// Act
336	result := pv.Overlay()
337
338	// Assert
339	assert.Equal(t, mockOverlay, result)
340}
341
342func TestShow_DelegatesToOverlay(t *testing.T) {
343	// Arrange
344	mockFactory := mocks.NewMockWidgetFactory(t)
345	mockOverlay := mocks.NewMockOverlayWidget(t)
346	mockBorderBox := mocks.NewMockBoxWidget(t)
347	mockWebView := mocks.NewMockWidget(t)
348
349	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
350
351	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
352
353	mockOverlay.EXPECT().Show().Once()
354
355	// Act
356	pv.Show()
357
358	// Assert - verified by mock expectations
359}
360
361func TestHide_DelegatesToOverlay(t *testing.T) {
362	// Arrange
363	mockFactory := mocks.NewMockWidgetFactory(t)
364	mockOverlay := mocks.NewMockOverlayWidget(t)
365	mockBorderBox := mocks.NewMockBoxWidget(t)
366	mockWebView := mocks.NewMockWidget(t)
367
368	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
369
370	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
371
372	mockOverlay.EXPECT().Hide().Once()
373
374	// Act
375	pv.Hide()
376
377	// Assert - verified by mock expectations
378}
379
380func TestSetVisible_DelegatesToOverlay(t *testing.T) {
381	// Arrange
382	mockFactory := mocks.NewMockWidgetFactory(t)
383	mockOverlay := mocks.NewMockOverlayWidget(t)
384	mockBorderBox := mocks.NewMockBoxWidget(t)
385	mockWebView := mocks.NewMockWidget(t)
386
387	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
388
389	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
390
391	mockOverlay.EXPECT().SetVisible(true).Once()
392
393	// Act
394	pv.SetVisible(true)
395
396	// Assert - verified by mock expectations
397}
398
399func TestIsVisible_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(mockFactory, mockOverlay, mockBorderBox, mockWebView)
407
408	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
409
410	mockOverlay.EXPECT().IsVisible().Return(true).Once()
411
412	// Act
413	result := pv.IsVisible()
414
415	// Assert
416	assert.True(t, result)
417}
418
419func TestAddCssClass_DelegatesToOverlay(t *testing.T) {
420	// Arrange
421	mockFactory := mocks.NewMockWidgetFactory(t)
422	mockOverlay := mocks.NewMockOverlayWidget(t)
423	mockBorderBox := mocks.NewMockBoxWidget(t)
424	mockWebView := mocks.NewMockWidget(t)
425
426	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
427
428	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
429
430	mockOverlay.EXPECT().AddCssClass("custom-class").Once()
431
432	// Act
433	pv.AddCssClass("custom-class")
434
435	// Assert - verified by mock expectations
436}
437
438func TestRemoveCssClass_DelegatesToOverlay(t *testing.T) {
439	// Arrange
440	mockFactory := mocks.NewMockWidgetFactory(t)
441	mockOverlay := mocks.NewMockOverlayWidget(t)
442	mockBorderBox := mocks.NewMockBoxWidget(t)
443	mockWebView := mocks.NewMockWidget(t)
444
445	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
446
447	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
448
449	mockOverlay.EXPECT().RemoveCssClass("custom-class").Once()
450
451	// Act
452	pv.RemoveCssClass("custom-class")
453
454	// Assert - verified by mock expectations
455}
456
457func TestSetOnFocusIn_SetsCallback(t *testing.T) {
458	// Arrange
459	mockFactory := mocks.NewMockWidgetFactory(t)
460	mockOverlay := mocks.NewMockOverlayWidget(t)
461	mockBorderBox := mocks.NewMockBoxWidget(t)
462	mockWebView := mocks.NewMockWidget(t)
463
464	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
465
466	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
467
468	callback := func(paneID entity.PaneID) {}
469
470	// Act
471	pv.SetOnFocusIn(callback)
472
473	// Assert - callback is set (we can't directly verify without triggering focus)
474	assert.NotNil(t, pv)
475}
476
477func TestSetOnFocusOut_SetsCallback(t *testing.T) {
478	// Arrange
479	mockFactory := mocks.NewMockWidgetFactory(t)
480	mockOverlay := mocks.NewMockOverlayWidget(t)
481	mockBorderBox := mocks.NewMockBoxWidget(t)
482	mockWebView := mocks.NewMockWidget(t)
483
484	setupPaneViewMocks(mockFactory, mockOverlay, mockBorderBox, mockWebView)
485
486	pv := component.NewPaneView(mockFactory, entity.PaneID("pane-1"), mockWebView)
487
488	callback := func(paneID entity.PaneID) {}
489
490	// Act
491	pv.SetOnFocusOut(callback)
492
493	// Assert - callback is set (we can't directly verify without triggering focus)
494	assert.NotNil(t, pv)
495}
496
497// Helper function to setup common mock expectations for PaneView creation
498func setupPaneViewMocks(
499	mockFactory *mocks.MockWidgetFactory,
500	mockOverlay *mocks.MockOverlayWidget,
501	mockBorderBox *mocks.MockBoxWidget,
502	mockWebView *mocks.MockWidget,
503) {
504	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
505	mockOverlay.EXPECT().SetHexpand(true).Once()
506	mockOverlay.EXPECT().SetVexpand(true).Once()
507	mockOverlay.EXPECT().SetVisible(true).Once()
508	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
509	mockOverlay.EXPECT().SetChild(mockWebView).Once()
510
511	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
512	mockBorderBox.EXPECT().SetCanFocus(false).Once()
513	mockBorderBox.EXPECT().SetCanTarget(false).Once()
514	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
515	mockBorderBox.EXPECT().SetHexpand(true).Once()
516	mockBorderBox.EXPECT().SetVexpand(true).Once()
517	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
518	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
519	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
520}
521
522func setupPaneViewMocksNoWebView(
523	mockFactory *mocks.MockWidgetFactory,
524	mockOverlay *mocks.MockOverlayWidget,
525	mockBorderBox *mocks.MockBoxWidget,
526) {
527	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
528	mockOverlay.EXPECT().SetHexpand(true).Once()
529	mockOverlay.EXPECT().SetVexpand(true).Once()
530	mockOverlay.EXPECT().SetVisible(true).Once()
531	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
532
533	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
534	mockBorderBox.EXPECT().SetCanFocus(false).Once()
535	mockBorderBox.EXPECT().SetCanTarget(false).Once()
536	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
537	mockBorderBox.EXPECT().SetHexpand(true).Once()
538	mockBorderBox.EXPECT().SetVexpand(true).Once()
539	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
540	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
541	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
542}

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