v0.20.0 c741a39

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

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