v0.33.1 9577fee

lifecycle_test.go RawHistoryFolder

1package content
2
3import (
4	"context"
5	"errors"
6	"testing"
7
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/mock"
10	"github.com/stretchr/testify/require"
11
12	"github.com/bnema/dumber/internal/application/port"
13	"github.com/bnema/dumber/internal/application/port/mocks"
14	"github.com/bnema/dumber/internal/domain/entity"
15	"github.com/bnema/dumber/internal/ui/component"
16	"github.com/bnema/dumber/internal/ui/layout"
17	layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks"
18)
19
20// newMinimalCoordinator returns a Coordinator with the maps required by
21// ReleaseWebView initialized so that calls to clearPendingAppearance,
22// titleMu / navOriginMu locks do not panic.
23func newMinimalCoordinator() *Coordinator {
24	return &Coordinator{
25		webViews:   make(map[entity.PaneID]port.WebView),
26		paneTitles: make(map[entity.PaneID]string),
27		navOrigins: make(map[entity.PaneID]string),
28	}
29}
30
31// ---------------------------------------------------------------------------
32// EnsureWebView
33// ---------------------------------------------------------------------------
34
35func TestLifecycle_EnsureWebView_ReusesExistingNonDestroyed(t *testing.T) {
36	t.Parallel()
37
38	wv := mocks.NewMockWebView(t)
39	wv.EXPECT().IsDestroyed().Return(false)
40	wv.EXPECT().ID().Return(port.WebViewID(101)).Once()
41
42	pool := mocks.NewMockWebViewPool(t)
43	// pool.Acquire must NOT be called
44
45	c := newMinimalCoordinator()
46	c.pool = pool
47	c.webViews[entity.PaneID("pane-1")] = wv
48
49	got, err := c.EnsureWebView(context.Background(), "pane-1")
50
51	require.NoError(t, err)
52	assert.Equal(t, wv, got)
53}
54
55func TestLifecycle_EnsureWebView_AcquiresFromPoolWhenNoneExists(t *testing.T) {
56	t.Parallel()
57
58	newWV := mocks.NewMockWebView(t)
59	newWV.EXPECT().SetCallbacks(mock.Anything).Maybe()
60	newWV.EXPECT().Generation().Return(uint64(0)).Maybe()
61	newWV.EXPECT().ID().Return(port.WebViewID(102)).Maybe()
62
63	pool := mocks.NewMockWebViewPool(t)
64	pool.EXPECT().Acquire(mock.Anything).Return(newWV, nil)
65
66	c := newMinimalCoordinator()
67	c.pool = pool
68
69	got, err := c.EnsureWebView(context.Background(), "pane-1")
70
71	require.NoError(t, err)
72	assert.Equal(t, newWV, got)
73	assert.Equal(t, newWV, c.webViews[entity.PaneID("pane-1")])
74}
75
76func TestLifecycle_EnsureWebView_AcquiresFromPoolWhenExistingIsDestroyed(t *testing.T) {
77	t.Parallel()
78
79	oldWV := mocks.NewMockWebView(t)
80	oldWV.EXPECT().IsDestroyed().Return(true)
81
82	newWV := mocks.NewMockWebView(t)
83	newWV.EXPECT().SetCallbacks(mock.Anything).Maybe()
84	newWV.EXPECT().Generation().Return(uint64(0)).Maybe()
85	newWV.EXPECT().ID().Return(port.WebViewID(103)).Maybe()
86
87	pool := mocks.NewMockWebViewPool(t)
88	pool.EXPECT().Acquire(mock.Anything).Return(newWV, nil)
89
90	c := newMinimalCoordinator()
91	c.pool = pool
92	c.webViews[entity.PaneID("pane-1")] = oldWV
93
94	got, err := c.EnsureWebView(context.Background(), "pane-1")
95
96	require.NoError(t, err)
97	assert.Equal(t, newWV, got)
98}
99
100func TestLifecycle_EnsureWebView_ErrorWhenPoolIsNil(t *testing.T) {
101	t.Parallel()
102
103	c := newMinimalCoordinator()
104	// pool remains nil
105
106	_, err := c.EnsureWebView(context.Background(), "pane-1")
107
108	require.Error(t, err)
109	assert.Contains(t, err.Error(), "webview pool not configured")
110}
111
112func TestLifecycle_EnsureWebView_ErrorWhenAcquireFails(t *testing.T) {
113	t.Parallel()
114
115	acquireErr := errors.New("pool exhausted")
116
117	pool := mocks.NewMockWebViewPool(t)
118	pool.EXPECT().Acquire(mock.Anything).Return(nil, acquireErr)
119
120	c := newMinimalCoordinator()
121	c.pool = pool
122
123	_, err := c.EnsureWebView(context.Background(), "pane-1")
124
125	require.Error(t, err)
126	assert.Equal(t, acquireErr, err)
127}
128
129func TestLifecycle_AttachToWorkspace_ReusesRegisteredWebViewByPaneID(t *testing.T) {
130	t.Parallel()
131
132	paneID := entity.PaneID("pane-1")
133	wv := mocks.NewMockWebView(t)
134	wv.EXPECT().IsDestroyed().Return(false).Once()
135	wv.EXPECT().ID().Return(port.WebViewID(104)).Once()
136	wv.EXPECT().URI().Return("").Maybe()
137
138	pool := mocks.NewMockWebViewPool(t)
139	widgetFactory := layoutmocks.NewMockWidgetFactory(t)
140	ctx, wsView := newAttachWorkspaceView(t, widgetFactory)
141	workspace := &entity.Workspace{
142		ID:           "workspace-1",
143		Root:         &entity.PaneNode{ID: "node-1", Pane: entity.NewPane(paneID)},
144		ActivePaneID: paneID,
145	}
146
147	c := newMinimalCoordinator()
148	c.pool = pool
149	c.widgetFactory = widgetFactory
150	c.webViews[paneID] = wv
151
152	c.AttachToWorkspace(ctx, workspace, wsView)
153
154	assert.Same(t, wv, c.webViews[paneID])
155}
156
157func newAttachWorkspaceView(t *testing.T, factory *layoutmocks.MockWidgetFactory) (context.Context, *component.WorkspaceView) {
158	t.Helper()
159	ctx := context.Background()
160	container := layoutmocks.NewMockBoxWidget(t)
161	overlay := layoutmocks.NewMockOverlayWidget(t)
162
163	factory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(container).Once()
164	container.EXPECT().SetHexpand(true).Once()
165	container.EXPECT().SetVexpand(true).Once()
166	container.EXPECT().SetVisible(true).Once()
167	factory.EXPECT().NewOverlay().Return(overlay).Once()
168	overlay.EXPECT().SetHexpand(true).Once()
169	overlay.EXPECT().SetVexpand(true).Once()
170	overlay.EXPECT().SetChild(container).Once()
171	overlay.EXPECT().SetVisible(true).Once()
172
173	return ctx, component.NewWorkspaceView(ctx, factory)
174}
175
176// ---------------------------------------------------------------------------
177// ReleaseWebView
178// ---------------------------------------------------------------------------
179
180func TestLifecycle_ReleaseWebView_ReleasesToPool(t *testing.T) {
181	t.Parallel()
182
183	wv := mocks.NewMockWebView(t)
184	wv.EXPECT().ID().Return(port.WebViewID(11)).Maybe()
185	// idleInhibitor is nil so these won't be called
186	wv.EXPECT().IsFullscreen().Return(false).Maybe()
187	wv.EXPECT().IsPlayingAudio().Return(false).Maybe()
188
189	pool := mocks.NewMockWebViewPool(t)
190	pool.EXPECT().Release(wv)
191
192	c := newMinimalCoordinator()
193	c.pool = pool
194	c.webViews[entity.PaneID("pane-1")] = wv
195
196	c.ReleaseWebView(context.Background(), "pane-1")
197
198	_, exists := c.webViews[entity.PaneID("pane-1")]
199	assert.False(t, exists, "webview should be removed from map after release")
200}
201
202func TestLifecycle_ReleaseWebView_ClearsNamedBrowsingContextRegistration(t *testing.T) {
203	t.Parallel()
204
205	paneID := entity.PaneID("pane-1")
206	windowID := "window-1"
207	wv := mocks.NewMockWebView(t)
208	wv.EXPECT().ID().Return(port.WebViewID(15)).Maybe()
209	wv.EXPECT().IsDestroyed().Return(false).Maybe()
210	wv.EXPECT().IsFullscreen().Return(false).Maybe()
211	wv.EXPECT().IsPlayingAudio().Return(false).Maybe()
212
213	pool := mocks.NewMockWebViewPool(t)
214	pool.EXPECT().Release(wv)
215
216	c := newMinimalCoordinator()
217	c.pool = pool
218	c.popups = newPopupManager()
219	c.popups.setWindowIDResolver(func(id entity.PaneID) (string, bool) {
220		if id == paneID {
221			return windowID, true
222		}
223		return "", false
224	})
225	c.webViews[paneID] = wv
226	c.popups.namedContexts.Register(windowID, windowID, "named-popup", paneID, wv.ID())
227
228	_, _, okBefore := c.popups.namedContexts.Lookup(windowID, "named-popup", c.getWebViewLocked, c.popups.windowIDForPane)
229	require.True(t, okBefore)
230
231	c.ReleaseWebView(context.Background(), paneID)
232
233	_, _, okAfter := c.popups.namedContexts.Lookup(windowID, "named-popup", c.getWebViewLocked, c.popups.windowIDForPane)
234	assert.False(t, okAfter)
235}
236
237func TestLifecycle_ReleaseWebView_NoopForUnknownPane(t *testing.T) {
238	t.Parallel()
239
240	pool := mocks.NewMockWebViewPool(t)
241	// pool.Release must NOT be called
242
243	c := newMinimalCoordinator()
244	c.pool = pool
245
246	// Should not panic
247	c.ReleaseWebView(context.Background(), "unknown-pane")
248}
249
250func TestLifecycle_ReleaseWebView_UninhibitsIdleIfFullscreen(t *testing.T) {
251	t.Parallel()
252
253	wv := mocks.NewMockWebView(t)
254	wv.EXPECT().ID().Return(port.WebViewID(12)).Maybe()
255	wv.EXPECT().IsFullscreen().Return(true)
256	wv.EXPECT().IsPlayingAudio().Return(false)
257
258	inhibitor := mocks.NewMockIdleInhibitor(t)
259	inhibitor.EXPECT().Uninhibit(mock.Anything).Return(nil)
260
261	pool := mocks.NewMockWebViewPool(t)
262	pool.EXPECT().Release(wv)
263
264	c := newMinimalCoordinator()
265	c.pool = pool
266	c.idleInhibitor = inhibitor
267	c.webViews[entity.PaneID("pane-1")] = wv
268
269	c.ReleaseWebView(context.Background(), "pane-1")
270}
271
272func TestLifecycle_ReleaseWebView_UninhibitsIdleIfPlayingAudio(t *testing.T) {
273	t.Parallel()
274
275	wv := mocks.NewMockWebView(t)
276	wv.EXPECT().ID().Return(port.WebViewID(13)).Maybe()
277	wv.EXPECT().IsFullscreen().Return(false)
278	wv.EXPECT().IsPlayingAudio().Return(true)
279
280	inhibitor := mocks.NewMockIdleInhibitor(t)
281	inhibitor.EXPECT().Uninhibit(mock.Anything).Return(nil)
282
283	pool := mocks.NewMockWebViewPool(t)
284	pool.EXPECT().Release(wv)
285
286	c := newMinimalCoordinator()
287	c.pool = pool
288	c.idleInhibitor = inhibitor
289	c.webViews[entity.PaneID("pane-1")] = wv
290
291	c.ReleaseWebView(context.Background(), "pane-1")
292}
293
294func TestLifecycle_ReleaseWebView_DestroysWebViewWhenPoolIsNil(t *testing.T) {
295	t.Parallel()
296
297	wv := mocks.NewMockWebView(t)
298	wv.EXPECT().ID().Return(port.WebViewID(14)).Maybe()
299	// idleInhibitor is nil so these won't be called
300	wv.EXPECT().IsFullscreen().Return(false).Maybe()
301	wv.EXPECT().IsPlayingAudio().Return(false).Maybe()
302	wv.EXPECT().Destroy()
303
304	c := newMinimalCoordinator()
305	// pool remains nil
306	c.webViews[entity.PaneID("pane-1")] = wv
307
308	c.ReleaseWebView(context.Background(), "pane-1")
309}
310
311// ---------------------------------------------------------------------------
312// GetWebView / RegisterPopupWebView
313// ---------------------------------------------------------------------------
314
315func TestLifecycle_GetWebView_ReturnsRegisteredWebView(t *testing.T) {
316	t.Parallel()
317
318	wv := mocks.NewMockWebView(t)
319
320	c := newMinimalCoordinator()
321	c.webViews[entity.PaneID("pane-1")] = wv
322
323	got := c.GetWebView("pane-1")
324
325	assert.Equal(t, wv, got)
326}
327
328func TestLifecycle_RegisterPopupWebView_StoresWebView(t *testing.T) {
329	t.Parallel()
330
331	wv := mocks.NewMockWebView(t)
332
333	c := newMinimalCoordinator()
334
335	c.RegisterPopupWebView("popup-1", wv)
336
337	assert.Equal(t, wv, c.GetWebView("popup-1"))
338}
339
340func TestLifecycle_RegisterPopupWebView_IgnoresNil(t *testing.T) {
341	t.Parallel()
342
343	c := newMinimalCoordinator()
344
345	// Should not panic
346	c.RegisterPopupWebView("popup-1", nil)
347
348	assert.Nil(t, c.GetWebView("popup-1"))
349}
350
351type nativeWebViewStub struct {
352	port.WebView
353	ptr uintptr
354}
355
356func (s *nativeWebViewStub) NativeWidget() uintptr {
357	return s.ptr
358}
359
360func TestLifecycle_WrapWidget_ReturnsNilForNilWebView(t *testing.T) {
361	t.Parallel()
362
363	factory := layoutmocks.NewMockWidgetFactory(t)
364	c := newMinimalCoordinator()
365	c.widgetFactory = factory
366
367	assert.Nil(t, c.WrapWidget(context.Background(), nil))
368}
369
370func TestLifecycle_WrapWidget_ReturnsNilForMissingNativeProvider(t *testing.T) {
371	t.Parallel()
372
373	wv := mocks.NewMockWebView(t)
374	factory := layoutmocks.NewMockWidgetFactory(t)
375	c := newMinimalCoordinator()
376	c.widgetFactory = factory
377
378	assert.Nil(t, c.WrapWidget(context.Background(), wv))
379}
380
381func TestLifecycle_WrapWidget_ReturnsNilForZeroNativePointer(t *testing.T) {
382	t.Parallel()
383
384	base := mocks.NewMockWebView(t)
385	wv := &nativeWebViewStub{WebView: base}
386	factory := layoutmocks.NewMockWidgetFactory(t)
387	c := newMinimalCoordinator()
388	c.widgetFactory = factory
389
390	assert.Nil(t, c.WrapWidget(context.Background(), wv))
391}
392
393func TestLifecycle_WrapWidget_ReturnsNilForNilWrappedWidget(t *testing.T) {
394	t.Parallel()
395
396	base := mocks.NewMockWebView(t)
397	wv := &nativeWebViewStub{WebView: base, ptr: 123}
398	factory := layoutmocks.NewMockWidgetFactory(t)
399	factory.EXPECT().WrapNativeWidget(uintptr(123)).Return(nil).Once()
400	c := newMinimalCoordinator()
401	c.widgetFactory = factory
402
403	assert.Nil(t, c.WrapWidget(context.Background(), wv))
404}
405
406func TestLifecycle_WrapWidget_RequestsGtkWidgetForGestureSetup(t *testing.T) {
407	t.Parallel()
408
409	base := mocks.NewMockWebView(t)
410	wv := &nativeWebViewStub{WebView: base, ptr: 456}
411	factory := layoutmocks.NewMockWidgetFactory(t)
412	widget := layoutmocks.NewMockWidget(t)
413	factory.EXPECT().WrapNativeWidget(uintptr(456)).Return(widget).Once()
414	widget.EXPECT().GtkWidget().Return(nil).Once()
415	c := newMinimalCoordinator()
416	c.widgetFactory = factory
417
418	assert.Same(t, widget, c.WrapWidget(context.Background(), wv))
419}
420
421type syncViewportContextKey struct{}
422
423type syncViewportCapableStub struct {
424	port.WebView
425	called bool
426	ctx    context.Context
427	reason string
428}
429
430func (s *syncViewportCapableStub) SyncViewport(ctx context.Context, reason string) {
431	s.called = true
432	s.ctx = ctx
433	s.reason = reason
434}
435
436func TestLifecycle_SyncWebViewViewport_DelegatesWhenCapabilityPresent(t *testing.T) {
437	t.Parallel()
438
439	base := mocks.NewMockWebView(t)
440	wv := &syncViewportCapableStub{WebView: base}
441	c := newMinimalCoordinator()
442	c.webViews[entity.PaneID("pane-1")] = wv
443
444	ctx := context.WithValue(context.Background(), syncViewportContextKey{}, "marker")
445	c.SyncWebViewViewport(ctx, "pane-1", "unit-test")
446
447	require.True(t, wv.called)
448	require.Equal(t, "unit-test", wv.reason)
449	require.Same(t, ctx, wv.ctx)
450}
451
452func TestLifecycle_SyncWebViewViewport_NoopWithoutCapability(t *testing.T) {
453	t.Parallel()
454
455	wv := mocks.NewMockWebView(t)
456	c := newMinimalCoordinator()
457	c.webViews[entity.PaneID("pane-1")] = wv
458
459	c.SyncWebViewViewport(context.Background(), "pane-1", "unit-test")
460}
461
462type nativeLifecycleWebView struct {
463	port.WebView
464	widget uintptr
465}
466
467func (w *nativeLifecycleWebView) NativeWidget() uintptr { return w.widget }
468
469func TestLifecycle_AttachToWorkspace_RebuildPropagatesRevealStateToFreshPaneView(t *testing.T) {
470	for _, tc := range []struct {
471		name     string
472		revealed bool
473	}{
474		{name: "revealed current identity hides and stops skeleton", revealed: true},
475		{name: "unrevealed current identity shows and starts skeleton", revealed: false},
476	} {
477		t.Run(tc.name, func(t *testing.T) {
478			paneID := entity.PaneID("pane-1")
479			ctx := context.Background()
480			factory := layoutmocks.NewMockWidgetFactory(t)
481			wsView, workspace, paneOverlay, loadingContainer, spinner := newLifecycleRebuildWorkspaceView(t, ctx, factory, paneID)
482			wrappedWidget := layoutmocks.NewMockWidget(t)
483			webViewMock := mocks.NewMockWebView(t)
484			webViewMock.EXPECT().ID().Return(port.WebViewID(201)).Maybe()
485			webViewMock.EXPECT().Generation().Return(uint64(3)).Maybe()
486			webViewMock.EXPECT().IsDestroyed().Return(false).Once()
487			webView := &nativeLifecycleWebView{WebView: webViewMock, widget: 0x351}
488
489			factory.EXPECT().WrapNativeWidget(uintptr(0x351)).Return(wrappedWidget).Once()
490			wrappedWidget.EXPECT().GtkWidget().Return(nil).Once()
491			wrappedWidget.EXPECT().GetAllocatedWidth().Return(0).Times(3)
492			wrappedWidget.EXPECT().GetAllocatedHeight().Return(0).Times(3)
493			wrappedWidget.EXPECT().GetParent().Return(nil).Once()
494			wrappedWidget.EXPECT().IsVisible().Return(true).Once()
495			paneOverlay.EXPECT().GetAllocatedWidth().Return(0).Twice()
496			paneOverlay.EXPECT().GetAllocatedHeight().Return(0).Twice()
497			paneOverlay.EXPECT().SetChild(wrappedWidget).Once()
498			loadingContainer.EXPECT().SetVisible(!tc.revealed).Once()
499			if tc.revealed {
500				spinner.EXPECT().Stop().Once()
501			} else {
502				spinner.EXPECT().Start().Once()
503			}
504
505			coordinator := newRevealTestCoordinator()
506			coordinator.widgetFactory = factory
507			coordinator.setWebViewLocked(paneID, webView)
508			if tc.revealed {
509				identity, ok := identityForWebView(webView)
510				require.True(t, ok)
511				coordinator.markWebViewRevealed(paneID, identity)
512			}
513
514			coordinator.AttachToWorkspace(ctx, workspace, wsView)
515
516			assert.Same(t, wrappedWidget, wsView.GetPaneView(paneID).WebViewWidget())
517		})
518	}
519}
520
521func newLifecycleRebuildWorkspaceView(
522	t *testing.T,
523	ctx context.Context,
524	factory *layoutmocks.MockWidgetFactory,
525	paneID entity.PaneID,
526) (*component.WorkspaceView, *entity.Workspace, *layoutmocks.MockOverlayWidget, *layoutmocks.MockBoxWidget, *layoutmocks.MockSpinnerWidget) {
527	t.Helper()
528	workspaceContainer := layoutmocks.NewMockBoxWidget(t)
529	workspaceOverlay := layoutmocks.NewMockOverlayWidget(t)
530	factory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(workspaceContainer).Once()
531	workspaceContainer.EXPECT().SetHexpand(true).Once()
532	workspaceContainer.EXPECT().SetVexpand(true).Once()
533	workspaceContainer.EXPECT().SetVisible(true).Once()
534	factory.EXPECT().NewOverlay().Return(workspaceOverlay).Once()
535	workspaceOverlay.EXPECT().SetHexpand(true).Once()
536	workspaceOverlay.EXPECT().SetVexpand(true).Once()
537	workspaceOverlay.EXPECT().SetChild(workspaceContainer).Once()
538	workspaceOverlay.EXPECT().SetVisible(true).Once()
539	wsView := component.NewWorkspaceView(ctx, factory)
540
541	paneOverlay := layoutmocks.NewMockOverlayWidget(t)
542	loadingContainer := layoutmocks.NewMockBoxWidget(t)
543	loadingContent := layoutmocks.NewMockBoxWidget(t)
544	spinner := layoutmocks.NewMockSpinnerWidget(t)
545	logo := layoutmocks.NewMockImageWidget(t)
546	version := layoutmocks.NewMockLabelWidget(t)
547	border := layoutmocks.NewMockBoxWidget(t)
548
549	factory.EXPECT().NewOverlay().Return(paneOverlay).Once()
550	paneOverlay.EXPECT().SetHexpand(true).Once()
551	paneOverlay.EXPECT().SetVexpand(true).Once()
552	paneOverlay.EXPECT().SetVisible(true).Once()
553	paneOverlay.EXPECT().AddCssClass("pane-overlay").Once()
554	factory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(loadingContainer).Once()
555	loadingContainer.EXPECT().SetHexpand(true).Once()
556	loadingContainer.EXPECT().SetVexpand(true).Once()
557	loadingContainer.EXPECT().SetHalign(mock.Anything).Once()
558	loadingContainer.EXPECT().SetValign(mock.Anything).Once()
559	loadingContainer.EXPECT().SetCanFocus(false).Times(2)
560	loadingContainer.EXPECT().SetCanTarget(false).Times(2)
561	loadingContainer.EXPECT().AddCssClass("loading-skeleton").Once()
562	factory.EXPECT().NewBox(layout.OrientationVertical, 6).Return(loadingContent).Once()
563	loadingContent.EXPECT().SetHalign(mock.Anything).Once()
564	loadingContent.EXPECT().SetValign(mock.Anything).Once()
565	loadingContent.EXPECT().SetCanFocus(false).Once()
566	loadingContent.EXPECT().SetCanTarget(false).Once()
567	loadingContent.EXPECT().AddCssClass("loading-skeleton-content").Once()
568	factory.EXPECT().NewImage().Return(logo).Once()
569	logo.EXPECT().SetHalign(mock.Anything).Once()
570	logo.EXPECT().SetValign(mock.Anything).Once()
571	logo.EXPECT().SetCanFocus(false).Once()
572	logo.EXPECT().SetCanTarget(false).Once()
573	logo.EXPECT().SetSizeRequest(256, 256).Once()
574	logo.EXPECT().SetPixelSize(256).Once()
575	logo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
576	logo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
577	factory.EXPECT().NewSpinner().Return(spinner).Once()
578	spinner.EXPECT().SetHalign(mock.Anything).Once()
579	spinner.EXPECT().SetValign(mock.Anything).Once()
580	spinner.EXPECT().SetCanFocus(false).Once()
581	spinner.EXPECT().SetCanTarget(false).Once()
582	spinner.EXPECT().SetSizeRequest(32, 32).Once()
583	spinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
584	factory.EXPECT().NewLabel(mock.Anything).Return(version).Once()
585	version.EXPECT().SetHalign(mock.Anything).Once()
586	version.EXPECT().SetValign(mock.Anything).Once()
587	version.EXPECT().SetCanFocus(false).Once()
588	version.EXPECT().SetCanTarget(false).Once()
589	version.EXPECT().SetMaxWidthChars(mock.Anything).Once()
590	version.EXPECT().SetEllipsize(mock.Anything).Once()
591	version.EXPECT().AddCssClass("loading-skeleton-version").Once()
592	loadingContent.EXPECT().Append(logo).Once()
593	loadingContent.EXPECT().Append(spinner).Once()
594	loadingContent.EXPECT().Append(version).Once()
595	loadingContainer.EXPECT().Append(loadingContent).Once()
596	loadingContainer.EXPECT().SetVisible(true).Once()
597	spinner.EXPECT().Start().Once()
598	paneOverlay.EXPECT().AddOverlay(loadingContainer).Once()
599	paneOverlay.EXPECT().SetClipOverlay(loadingContainer, false).Once()
600	paneOverlay.EXPECT().SetMeasureOverlay(loadingContainer, false).Once()
601	factory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(border).Once()
602	border.EXPECT().SetCanFocus(false).Once()
603	border.EXPECT().SetCanTarget(false).Once()
604	border.EXPECT().AddCssClass("pane-border").Once()
605	border.EXPECT().SetHexpand(true).Once()
606	border.EXPECT().SetVexpand(true).Once()
607	paneOverlay.EXPECT().AddOverlay(border).Once()
608	paneOverlay.EXPECT().SetClipOverlay(border, false).Once()
609	paneOverlay.EXPECT().SetMeasureOverlay(border, false).Once()
610
611	stackBox := layoutmocks.NewMockBoxWidget(t)
612	titleBar := layoutmocks.NewMockBoxWidget(t)
613	favicon := layoutmocks.NewMockImageWidget(t)
614	title := layoutmocks.NewMockLabelWidget(t)
615	closeButton := layoutmocks.NewMockButtonWidget(t)
616	factory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(stackBox).Once()
617	stackBox.EXPECT().SetHexpand(true).Once()
618	stackBox.EXPECT().SetVexpand(true).Once()
619	factory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(titleBar).Once()
620	titleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
621	titleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
622	titleBar.EXPECT().SetVexpand(false).Once()
623	titleBar.EXPECT().SetHexpand(true).Once()
624	factory.EXPECT().NewImage().Return(favicon).Once()
625	favicon.EXPECT().SetFromIconName(mock.Anything).Once()
626	favicon.EXPECT().SetPixelSize(16).Once()
627	titleBar.EXPECT().Append(favicon).Once()
628	factory.EXPECT().NewLabel(mock.Anything).Return(title).Once()
629	title.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
630	title.EXPECT().SetMaxWidthChars(30).Once()
631	title.EXPECT().SetHexpand(true).Once()
632	title.EXPECT().SetXalign(float32(0)).Once()
633	titleBar.EXPECT().Append(title).Once()
634	factory.EXPECT().NewButton().Return(closeButton).Once()
635	closeButton.EXPECT().SetIconName("window-close-symbolic").Once()
636	closeButton.EXPECT().AddCssClass("stacked-pane-close-button").Once()
637	closeButton.EXPECT().SetFocusOnClick(false).Once()
638	closeButton.EXPECT().SetVexpand(false).Once()
639	closeButton.EXPECT().SetHexpand(false).Once()
640	titleBar.EXPECT().Append(closeButton).Once()
641	titleBar.EXPECT().AddController(mock.Anything).Once()
642	closeButton.EXPECT().ConnectClicked(mock.Anything).Return(uint(2)).Once()
643	closeButton.EXPECT().GtkWidget().Return(nil).Maybe()
644	stackBox.EXPECT().Append(titleBar).Once()
645	stackBox.EXPECT().Append(paneOverlay).Once()
646	titleBar.EXPECT().SetVisible(false).Once()
647	paneOverlay.EXPECT().SetVisible(true).Once()
648	titleBar.EXPECT().AddCssClass("active").Once()
649	paneOverlay.EXPECT().GtkWidget().Return(nil).Maybe()
650	stackBox.EXPECT().SetVisible(true).Once()
651	workspaceContainer.EXPECT().Append(stackBox).Once()
652	workspaceContainer.EXPECT().AddCssClass("single-pane").Once()
653	border.EXPECT().AddCssClass("pane-active").Once()
654
655	workspace := &entity.Workspace{
656		ID:           "workspace-1",
657		Root:         &entity.PaneNode{ID: "node-1", Pane: entity.NewPane(paneID)},
658		ActivePaneID: paneID,
659	}
660	require.NoError(t, wsView.SetWorkspace(ctx, workspace))
661	return wsView, workspace, paneOverlay, loadingContainer, spinner
662}

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