main 5041353

workspace_view_test.go RawHistoryFolder

1package component_test
2
3import (
4	"context"
5	"testing"
6
7	"github.com/bnema/puregotk/v4/gtk"
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/domain/entity"
13	"github.com/bnema/dumber/internal/ui/component"
14	"github.com/bnema/dumber/internal/ui/layout"
15	"github.com/bnema/dumber/internal/ui/layout/mocks"
16)
17
18func setupLoadingSkeletonMocksWorkspace(
19	t *testing.T,
20	mockFactory *mocks.MockWidgetFactory,
21	mockOverlay *mocks.MockOverlayWidget,
22	mockLoadingContainer *mocks.MockBoxWidget,
23	mockLoadingContent *mocks.MockBoxWidget,
24	mockLoadingSpinner *mocks.MockSpinnerWidget,
25	mockLoadingLogo *mocks.MockImageWidget,
26) {
27	mockLoadingVersion := mocks.NewMockLabelWidget(t)
28
29	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
30	mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
31	mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()
32	mockLoadingContainer.EXPECT().SetHalign(mock.Anything).Maybe()
33	mockLoadingContainer.EXPECT().SetValign(mock.Anything).Maybe()
34	mockLoadingContainer.EXPECT().SetCanFocus(false).Maybe()
35	mockLoadingContainer.EXPECT().SetCanTarget(false).Maybe()
36	mockLoadingContainer.EXPECT().AddCssClass("loading-skeleton").Maybe()
37	mockLoadingContainer.EXPECT().SetVisible(true).Maybe()
38
39	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 6).Return(mockLoadingContent).Once()
40	mockLoadingContent.EXPECT().SetHalign(mock.Anything).Maybe()
41	mockLoadingContent.EXPECT().SetValign(mock.Anything).Maybe()
42	mockLoadingContent.EXPECT().SetCanFocus(false).Maybe()
43	mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()
44	mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()
45
46	mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
47	mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
48	mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
49	mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
50	mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
51	mockLoadingLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Maybe()
52	mockLoadingLogo.EXPECT().SetPixelSize(mock.Anything).Maybe()
53	mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
54	mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
55
56	mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()
57	mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()
58	mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()
59	mockLoadingSpinner.EXPECT().SetCanFocus(false).Maybe()
60	mockLoadingSpinner.EXPECT().SetCanTarget(false).Maybe()
61	mockLoadingSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Maybe()
62	mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
63	mockLoadingSpinner.EXPECT().Start().Maybe()
64
65	mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLoadingVersion).Once()
66	mockLoadingVersion.EXPECT().SetHalign(mock.Anything).Maybe()
67	mockLoadingVersion.EXPECT().SetValign(mock.Anything).Maybe()
68	mockLoadingVersion.EXPECT().SetCanFocus(false).Maybe()
69	mockLoadingVersion.EXPECT().SetCanTarget(false).Maybe()
70	mockLoadingVersion.EXPECT().SetMaxWidthChars(mock.Anything).Maybe()
71	mockLoadingVersion.EXPECT().SetEllipsize(mock.Anything).Maybe()
72	mockLoadingVersion.EXPECT().AddCssClass("loading-skeleton-version").Maybe()
73
74	mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
75	mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
76	mockLoadingContent.EXPECT().Append(mockLoadingVersion).Maybe()
77	mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
78
79	mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
80	mockOverlay.EXPECT().SetClipOverlay(mockLoadingContainer, false).Once()
81	mockOverlay.EXPECT().SetMeasureOverlay(mockLoadingContainer, false).Once()
82}
83
84func setupWorkspaceViewBase(t *testing.T, mockFactory *mocks.MockWidgetFactory) (context.Context, *mocks.MockBoxWidget) {
85	ctx := context.Background()
86	mockContainer := mocks.NewMockBoxWidget(t)
87	mockOverlay := mocks.NewMockOverlayWidget(t)
88
89	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
90	mockContainer.EXPECT().SetHexpand(true).Once()
91	mockContainer.EXPECT().SetVexpand(true).Once()
92	mockContainer.EXPECT().SetVisible(true).Once()
93
94	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
95	mockOverlay.EXPECT().SetHexpand(true).Once()
96	mockOverlay.EXPECT().SetVexpand(true).Once()
97	mockOverlay.EXPECT().SetChild(mockContainer).Once()
98	mockOverlay.EXPECT().SetVisible(true).Once()
99
100	return ctx, mockContainer
101}
102
103func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory) (*mocks.MockOverlayWidget, *mocks.MockBoxWidget) {
104	mockOverlay := mocks.NewMockOverlayWidget(t)
105	mockBorderBox := mocks.NewMockBoxWidget(t)
106
107	mockLoadingContainer := mocks.NewMockBoxWidget(t)
108	mockLoadingContent := mocks.NewMockBoxWidget(t)
109	mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
110	mockLoadingLogo := mocks.NewMockImageWidget(t)
111
112	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
113	mockOverlay.EXPECT().SetHexpand(true).Once()
114	mockOverlay.EXPECT().SetVexpand(true).Once()
115	mockOverlay.EXPECT().SetVisible(true).Once()
116	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()
117
118	setupLoadingSkeletonMocksWorkspace(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
119
120	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
121	mockBorderBox.EXPECT().SetCanFocus(false).Once()
122	mockBorderBox.EXPECT().SetCanTarget(false).Once()
123	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
124	mockBorderBox.EXPECT().SetHexpand(true).Once()
125	mockBorderBox.EXPECT().SetVexpand(true).Once()
126	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
127	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
128	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
129
130	// GtkWidget is called when attaching hover handler - return nil for tests
131	mockOverlay.EXPECT().GtkWidget().Return(nil).Maybe()
132
133	return mockOverlay, mockBorderBox
134}
135
136func setupStackedLeafMocks(
137	t *testing.T,
138	mockFactory *mocks.MockWidgetFactory,
139	container *mocks.MockOverlayWidget,
140) *mocks.MockBoxWidget {
141	mockStackBox := mocks.NewMockBoxWidget(t)
142	mockTitleBar := mocks.NewMockBoxWidget(t)
143	mockFavicon := mocks.NewMockImageWidget(t)
144	mockLabel := mocks.NewMockLabelWidget(t)
145	mockCloseButton := mocks.NewMockButtonWidget(t)
146
147	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockStackBox).Once()
148	mockStackBox.EXPECT().SetHexpand(true).Once()
149	mockStackBox.EXPECT().SetVexpand(true).Once()
150
151	// Title bar creation - now directly used without button wrapper
152	mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
153	mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
154	mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
155	mockTitleBar.EXPECT().SetVexpand(false).Once()
156	mockTitleBar.EXPECT().SetHexpand(true).Once()
157
158	mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
159	mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
160	mockFavicon.EXPECT().SetPixelSize(16).Once()
161	mockTitleBar.EXPECT().Append(mockFavicon).Once()
162
163	mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once()
164	mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
165	mockLabel.EXPECT().SetMaxWidthChars(30).Once()
166	mockLabel.EXPECT().SetHexpand(true).Once()
167	mockLabel.EXPECT().SetXalign(float32(0.0)).Once()
168	mockTitleBar.EXPECT().Append(mockLabel).Once()
169
170	// Close button (uses SetIconName directly instead of child image)
171	mockFactory.EXPECT().NewButton().Return(mockCloseButton).Once()
172	mockCloseButton.EXPECT().SetIconName("window-close-symbolic").Once()
173	mockCloseButton.EXPECT().AddCssClass("stacked-pane-close-button").Once()
174	mockCloseButton.EXPECT().SetFocusOnClick(false).Once()
175	mockCloseButton.EXPECT().SetVexpand(false).Once()
176	mockCloseButton.EXPECT().SetHexpand(false).Once()
177	mockTitleBar.EXPECT().Append(mockCloseButton).Once()
178
179	// GestureClick is added to titleBar via AddController
180	mockTitleBar.EXPECT().AddController(mock.Anything).Once()
181
182	// Close button click handler
183	mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint(2)).Once()
184
185	// Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
186	mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
187
188	// Adding to main box - now titleBar is added directly (no button wrapper)
189	mockStackBox.EXPECT().Append(mockTitleBar).Once()
190	mockStackBox.EXPECT().Append(container).Once()
191
192	// Visibility updates for active pane - titleBar is now directly in box, SetVisible called on it
193	mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
194	container.EXPECT().SetVisible(true).Once()
195	mockTitleBar.EXPECT().AddCssClass("active").Once()
196
197	return mockStackBox
198}
199
200func TestNewWorkspaceView_CreatesEmptyContainer(t *testing.T) {
201	// Arrange
202	mockFactory := mocks.NewMockWidgetFactory(t)
203	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
204
205	// Act
206	wv := component.NewWorkspaceView(ctx, mockFactory)
207
208	// Assert
209	require.NotNil(t, wv)
210	assert.Equal(t, 0, wv.PaneCount())
211	assert.Equal(t, mockBox, wv.Container())
212}
213
214func TestSetWorkspace_NilWorkspace_ReturnsError(t *testing.T) {
215	// Arrange
216	mockFactory := mocks.NewMockWidgetFactory(t)
217	ctx, _ := setupWorkspaceViewBase(t, mockFactory)
218
219	wv := component.NewWorkspaceView(ctx, mockFactory)
220
221	// Act
222	err := wv.SetWorkspace(ctx, nil)
223
224	// Assert
225	assert.ErrorIs(t, err, component.ErrNilWorkspace)
226}
227
228func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) {
229	// Arrange
230	mockFactory := mocks.NewMockWidgetFactory(t)
231	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
232
233	wv := component.NewWorkspaceView(ctx, mockFactory)
234
235	mockOverlay, mockBorderBox := setupWorkspacePaneViewMocks(t, mockFactory)
236	mockStackBox := setupStackedLeafMocks(t, mockFactory, mockOverlay)
237
238	mockStackBox.EXPECT().SetVisible(true).Once()
239	mockBox.EXPECT().Append(mockStackBox).Once()
240	mockBox.EXPECT().AddCssClass("single-pane").Once()
241	mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
242
243	pane := entity.NewPane(entity.PaneID("pane-1"))
244	node := &entity.PaneNode{ID: "node-1", Pane: pane}
245
246	ws := &entity.Workspace{
247		ID:           "ws-1",
248		Root:         node,
249		ActivePaneID: pane.ID,
250	}
251
252	// Act
253	err := wv.SetWorkspace(ctx, ws)
254
255	// Assert
256	require.NoError(t, err)
257	assert.Equal(t, 1, wv.PaneCount())
258	assert.Equal(t, pane.ID, wv.GetActivePaneID())
259	assert.NotNil(t, wv.GetPaneView(pane.ID))
260}
261
262func TestSetWorkspace_StaleActivePaneID_DoesNotFailInitialization(t *testing.T) {
263	// Arrange
264	mockFactory := mocks.NewMockWidgetFactory(t)
265	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
266
267	wv := component.NewWorkspaceView(ctx, mockFactory)
268
269	mockOverlay, mockBorderBox := setupWorkspacePaneViewMocks(t, mockFactory)
270	mockStackBox := setupStackedLeafMocks(t, mockFactory, mockOverlay)
271
272	mockStackBox.EXPECT().SetVisible(true).Once()
273	mockBox.EXPECT().Append(mockStackBox).Once()
274	mockBox.EXPECT().AddCssClass("single-pane").Once()
275	mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
276
277	pane := entity.NewPane(entity.PaneID("pane-1"))
278	node := &entity.PaneNode{ID: "node-1", Pane: pane}
279	ws := &entity.Workspace{
280		ID:           "ws-1",
281		Root:         node,
282		ActivePaneID: entity.PaneID("stale-pane"),
283	}
284
285	// Act
286	err := wv.SetWorkspace(ctx, ws)
287
288	// Assert
289	require.NoError(t, err)
290	assert.Equal(t, 1, wv.PaneCount())
291	assert.Equal(t, pane.ID, wv.GetActivePaneID())
292	assert.True(t, wv.GetPaneView(pane.ID).IsActive())
293}
294
295func TestSetWorkspace_StaleActivePaneID_UsesFirstPaneInTree(t *testing.T) {
296	// Arrange
297	mockFactory := mocks.NewMockWidgetFactory(t)
298	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
299	wv := component.NewWorkspaceView(ctx, mockFactory)
300
301	pane1 := entity.NewPane(entity.PaneID("pane-1"))
302	pane2 := entity.NewPane(entity.PaneID("pane-2"))
303	node1 := &entity.PaneNode{ID: "node-1", Pane: pane1}
304	node2 := &entity.PaneNode{ID: "node-2", Pane: pane2}
305
306	mockPaned := mocks.NewMockPanedWidget(t)
307	mockOverlay1, mockBorderBox1 := setupWorkspacePaneViewMocks(t, mockFactory)
308	mockStackBox1 := setupStackedLeafMocks(t, mockFactory, mockOverlay1)
309	mockOverlay2, _ := setupWorkspacePaneViewMocks(t, mockFactory)
310	mockStackBox2 := setupStackedLeafMocks(t, mockFactory, mockOverlay2)
311
312	mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
313	mockPaned.EXPECT().SetResizeStartChild(true).Once()
314	mockPaned.EXPECT().SetResizeEndChild(true).Once()
315	mockPaned.EXPECT().SetVisible(true).Twice()
316	mockStackBox1.EXPECT().SetVisible(true).Once()
317	mockStackBox2.EXPECT().SetVisible(true).Once()
318	mockPaned.EXPECT().SetStartChild(mockStackBox1).Once()
319	mockPaned.EXPECT().SetEndChild(mockStackBox2).Once()
320	mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint(0)).Once()
321	mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
322	mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint(0)).Once()
323	mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
324	mockBox.EXPECT().Append(mockPaned).Once()
325	mockBox.EXPECT().RemoveCssClass("single-pane").Once()
326	mockBorderBox1.EXPECT().AddCssClass("pane-active").Once()
327
328	ws := &entity.Workspace{
329		ID: "ws-1",
330		Root: &entity.PaneNode{
331			ID:         "split",
332			SplitDir:   entity.SplitHorizontal,
333			SplitRatio: 0.5,
334			Children:   []*entity.PaneNode{node1, node2},
335		},
336		ActivePaneID: entity.PaneID("stale-pane"),
337	}
338
339	// Act
340	err := wv.SetWorkspace(ctx, ws)
341
342	// Assert
343	require.NoError(t, err)
344	assert.Equal(t, pane1.ID, wv.GetActivePaneID())
345	assert.True(t, wv.GetPaneView(pane1.ID).IsActive())
346	assert.False(t, wv.GetPaneView(pane2.ID).IsActive())
347}
348
349func TestWorkspaceView_HoverFocusLockState(t *testing.T) {
350	// Intentional zero-value check: hoverFocusLock is an atomic.Bool and should
351	// be safe with its language-level zero value before constructor wiring.
352	var wv component.WorkspaceView
353
354	assert.False(t, wv.IsHoverFocusLocked())
355	wv.SetHoverFocusLocked(true)
356	assert.True(t, wv.IsHoverFocusLocked())
357
358	wv.SetHoverFocusLocked(false)
359	assert.False(t, wv.IsHoverFocusLocked())
360}
361
362func TestAddWorkspaceOverlayWidget_DoesNotReaddWhenAlreadyAttached(t *testing.T) {
363	mockFactory := mocks.NewMockWidgetFactory(t)
364	ctx := context.Background()
365
366	mockContainer := mocks.NewMockBoxWidget(t)
367	mockOverlay := mocks.NewMockOverlayWidget(t)
368	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
369	mockContainer.EXPECT().SetHexpand(true).Once()
370	mockContainer.EXPECT().SetVexpand(true).Once()
371	mockContainer.EXPECT().SetVisible(true).Once()
372	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
373	mockOverlay.EXPECT().SetHexpand(true).Once()
374	mockOverlay.EXPECT().SetVexpand(true).Once()
375	mockOverlay.EXPECT().SetChild(mockContainer).Once()
376	mockOverlay.EXPECT().SetVisible(true).Once()
377
378	wv := component.NewWorkspaceView(ctx, mockFactory)
379
380	widget := mocks.NewMockWidget(t)
381	widget.EXPECT().GetParent().Return(mockOverlay).Once()
382
383	wv.AddWorkspaceOverlayWidget(widget)
384}
385
386func TestAddWorkspaceOverlayWidget_UnparentsBeforeAdding(t *testing.T) {
387	mockFactory := mocks.NewMockWidgetFactory(t)
388	ctx := context.Background()
389
390	mockContainer := mocks.NewMockBoxWidget(t)
391	mockOverlay := mocks.NewMockOverlayWidget(t)
392	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
393	mockContainer.EXPECT().SetHexpand(true).Once()
394	mockContainer.EXPECT().SetVexpand(true).Once()
395	mockContainer.EXPECT().SetVisible(true).Once()
396	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
397	mockOverlay.EXPECT().SetHexpand(true).Once()
398	mockOverlay.EXPECT().SetVexpand(true).Once()
399	mockOverlay.EXPECT().SetChild(mockContainer).Once()
400	mockOverlay.EXPECT().SetVisible(true).Once()
401
402	wv := component.NewWorkspaceView(ctx, mockFactory)
403
404	widget := mocks.NewMockWidget(t)
405	otherParent := mocks.NewMockWidget(t)
406	widget.EXPECT().GetParent().Return(otherParent).Once()
407	otherParent.EXPECT().GtkWidget().Return(nil).Once()
408	mockOverlay.EXPECT().GtkWidget().Return(nil).Once()
409	widget.EXPECT().Unparent().Once()
410	mockOverlay.EXPECT().AddOverlay(widget).Once()
411	mockOverlay.EXPECT().SetClipOverlay(widget, false).Once()
412	mockOverlay.EXPECT().SetMeasureOverlay(widget, false).Once()
413
414	wv.AddWorkspaceOverlayWidget(widget)
415}
416
417func TestAddWorkspaceOverlayWidget_DoesNotReaddWhenParentGTKMatchesOverlay(t *testing.T) {
418	mockFactory := mocks.NewMockWidgetFactory(t)
419	ctx := context.Background()
420
421	mockContainer := mocks.NewMockBoxWidget(t)
422	mockOverlay := mocks.NewMockOverlayWidget(t)
423	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
424	mockContainer.EXPECT().SetHexpand(true).Once()
425	mockContainer.EXPECT().SetVexpand(true).Once()
426	mockContainer.EXPECT().SetVisible(true).Once()
427	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
428	mockOverlay.EXPECT().SetHexpand(true).Once()
429	mockOverlay.EXPECT().SetVexpand(true).Once()
430	mockOverlay.EXPECT().SetChild(mockContainer).Once()
431	mockOverlay.EXPECT().SetVisible(true).Once()
432
433	wv := component.NewWorkspaceView(ctx, mockFactory)
434
435	widget := mocks.NewMockWidget(t)
436	otherParent := mocks.NewMockWidget(t)
437	gtkWidget := &gtk.Widget{}
438
439	widget.EXPECT().GetParent().Return(otherParent).Once()
440	otherParent.EXPECT().GtkWidget().Return(gtkWidget).Once()
441	mockOverlay.EXPECT().GtkWidget().Return(gtkWidget).Once()
442
443	wv.AddWorkspaceOverlayWidget(widget)
444}
445
446func TestSetActivePaneID_UpdatesStyling(t *testing.T) {
447	// Arrange
448	mockFactory := mocks.NewMockWidgetFactory(t)
449	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
450
451	wv := component.NewWorkspaceView(ctx, mockFactory)
452
453	// Setup two panes
454	pane1 := entity.NewPane(entity.PaneID("pane-1"))
455	pane2 := entity.NewPane(entity.PaneID("pane-2"))
456	node1 := &entity.PaneNode{ID: "node-1", Pane: pane1}
457	node2 := &entity.PaneNode{ID: "node-2", Pane: pane2}
458
459	// Create mock widgets for split
460	mockPaned := mocks.NewMockPanedWidget(t)
461	mockOverlay1, mockBorderBox1 := setupWorkspacePaneViewMocks(t, mockFactory)
462	mockStackBox1 := setupStackedLeafMocks(t, mockFactory, mockOverlay1)
463	mockOverlay2, mockBorderBox2 := setupWorkspacePaneViewMocks(t, mockFactory)
464	mockStackBox2 := setupStackedLeafMocks(t, mockFactory, mockOverlay2)
465
466	// Split view creation
467	mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
468	mockPaned.EXPECT().SetResizeStartChild(true).Once()
469	mockPaned.EXPECT().SetResizeEndChild(true).Once()
470	mockPaned.EXPECT().SetVisible(true).Twice()
471	mockStackBox1.EXPECT().SetVisible(true).Once()
472	mockStackBox2.EXPECT().SetVisible(true).Once()
473	mockPaned.EXPECT().SetStartChild(mockStackBox1).Once()
474	mockPaned.EXPECT().SetEndChild(mockStackBox2).Once()
475	mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint(0)).Once()
476	mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
477	mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint(0)).Once()
478	mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
479
480	mockBox.EXPECT().Append(mockPaned).Once()
481
482	// Initial active pane is pane-1
483	mockBox.EXPECT().RemoveCssClass("single-pane").Once()
484	mockBorderBox1.EXPECT().AddCssClass("pane-active").Once()
485
486	splitNode := &entity.PaneNode{
487		ID:         "split",
488		SplitDir:   entity.SplitHorizontal,
489		SplitRatio: 0.5,
490		Children:   []*entity.PaneNode{node1, node2},
491	}
492
493	ws := &entity.Workspace{
494		ID:           "ws-1",
495		Root:         splitNode,
496		ActivePaneID: pane1.ID,
497	}
498
499	err := wv.SetWorkspace(ctx, ws)
500	require.NoError(t, err)
501
502	// Now change active pane
503	mockBorderBox1.EXPECT().RemoveCssClass("pane-active").Once()
504	mockBorderBox2.EXPECT().AddCssClass("pane-active").Once()
505	activatedPane := entity.PaneID("")
506	wv.SetOnActivePaneChanged(func(paneID entity.PaneID) {
507		activatedPane = paneID
508	})
509
510	// Act
511	err = wv.SetActivePaneID(pane2.ID)
512
513	// Assert
514	require.NoError(t, err)
515	assert.Equal(t, pane2.ID, wv.GetActivePaneID())
516	assert.Equal(t, pane2.ID, activatedPane)
517}
518
519func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) {
520	// Arrange
521	mockFactory := mocks.NewMockWidgetFactory(t)
522	ctx, _ := setupWorkspaceViewBase(t, mockFactory)
523
524	wv := component.NewWorkspaceView(ctx, mockFactory)
525
526	// Act
527	err := wv.SetActivePaneID(entity.PaneID("non-existent"))
528
529	// Assert
530	assert.ErrorIs(t, err, component.ErrPaneNotFound)
531}
532
533func TestSetActivePaneID_NilRegisteredPaneView_ReturnsError(t *testing.T) {
534	// Arrange
535	mockFactory := mocks.NewMockWidgetFactory(t)
536	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
537
538	wv := component.NewWorkspaceView(ctx, mockFactory)
539	mockBox.EXPECT().AddCssClass("single-pane").Once()
540	wv.RegisterPaneView(entity.PaneID("pane-1"), nil)
541
542	// Act
543	err := wv.SetActivePaneID(entity.PaneID("pane-1"))
544
545	// Assert
546	require.ErrorIs(t, err, component.ErrPaneNotFound)
547}
548
549func TestSetWebViewWidget_NilRegisteredPaneView_ReturnsError(t *testing.T) {
550	// Arrange
551	mockFactory := mocks.NewMockWidgetFactory(t)
552	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
553
554	wv := component.NewWorkspaceView(ctx, mockFactory)
555	mockBox.EXPECT().AddCssClass("single-pane").Once()
556	wv.RegisterPaneView(entity.PaneID("pane-1"), nil)
557
558	// Act
559	err := wv.SetWebViewWidget(entity.PaneID("pane-1"), mocks.NewMockWidget(t))
560
561	// Assert
562	require.ErrorIs(t, err, component.ErrPaneNotFound)
563}
564
565func TestSetActivePaneID_InvalidID_PreservesCurrentActivePane(t *testing.T) {
566	// Arrange
567	mockFactory := mocks.NewMockWidgetFactory(t)
568	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
569
570	wv := component.NewWorkspaceView(ctx, mockFactory)
571
572	pane1 := entity.NewPane(entity.PaneID("pane-1"))
573	pane2 := entity.NewPane(entity.PaneID("pane-2"))
574	node1 := &entity.PaneNode{ID: "node-1", Pane: pane1}
575	node2 := &entity.PaneNode{ID: "node-2", Pane: pane2}
576
577	mockPaned := mocks.NewMockPanedWidget(t)
578	mockOverlay1, mockBorderBox1 := setupWorkspacePaneViewMocks(t, mockFactory)
579	mockStackBox1 := setupStackedLeafMocks(t, mockFactory, mockOverlay1)
580	mockOverlay2, _ := setupWorkspacePaneViewMocks(t, mockFactory)
581	mockStackBox2 := setupStackedLeafMocks(t, mockFactory, mockOverlay2)
582
583	mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
584	mockPaned.EXPECT().SetResizeStartChild(true).Once()
585	mockPaned.EXPECT().SetResizeEndChild(true).Once()
586	mockPaned.EXPECT().SetVisible(true).Twice()
587	mockStackBox1.EXPECT().SetVisible(true).Once()
588	mockStackBox2.EXPECT().SetVisible(true).Once()
589	mockPaned.EXPECT().SetStartChild(mockStackBox1).Once()
590	mockPaned.EXPECT().SetEndChild(mockStackBox2).Once()
591	mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint(0)).Once()
592	mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
593	mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint(0)).Once()
594	mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
595
596	mockBox.EXPECT().Append(mockPaned).Once()
597	mockBox.EXPECT().RemoveCssClass("single-pane").Once()
598	mockBorderBox1.EXPECT().AddCssClass("pane-active").Once()
599
600	splitNode := &entity.PaneNode{
601		ID:         "split",
602		SplitDir:   entity.SplitHorizontal,
603		SplitRatio: 0.5,
604		Children:   []*entity.PaneNode{node1, node2},
605	}
606	ws := &entity.Workspace{
607		ID:           "ws-1",
608		Root:         splitNode,
609		ActivePaneID: pane1.ID,
610	}
611
612	err := wv.SetWorkspace(ctx, ws)
613	require.NoError(t, err)
614	require.True(t, wv.GetPaneView(pane1.ID).IsActive())
615
616	// Act
617	err = wv.SetActivePaneID(entity.PaneID("non-existent"))
618
619	// Assert
620	require.ErrorIs(t, err, component.ErrPaneNotFound)
621	assert.Equal(t, pane1.ID, wv.GetActivePaneID())
622	assert.True(t, wv.GetPaneView(pane1.ID).IsActive())
623}
624
625func TestSetWebViewWidget_InvokesAttachedCallback(t *testing.T) {
626	mockFactory := mocks.NewMockWidgetFactory(t)
627	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
628
629	wv := component.NewWorkspaceView(ctx, mockFactory)
630	mockOverlay, _ := setupWorkspacePaneViewMocks(t, mockFactory)
631	pv := component.NewPaneView(ctx, mockFactory, entity.PaneID("pane-1"), nil)
632	mockBox.EXPECT().AddCssClass("single-pane").Once()
633	wv.RegisterPaneView(entity.PaneID("pane-1"), pv)
634
635	attachedPane := entity.PaneID("")
636	wv.SetOnWebViewAttached(func(paneID entity.PaneID) {
637		attachedPane = paneID
638	})
639
640	mockWebViewWidget := mocks.NewMockWidget(t)
641	mockOverlay.EXPECT().GetAllocatedWidth().Return(0).Twice()
642	mockOverlay.EXPECT().GetAllocatedHeight().Return(0).Twice()
643	mockWebViewWidget.EXPECT().GetParent().Return(nil).Once()
644	mockWebViewWidget.EXPECT().IsVisible().Return(false).Once()
645	mockWebViewWidget.EXPECT().GetAllocatedWidth().Return(0).Twice()
646	mockWebViewWidget.EXPECT().GetAllocatedHeight().Return(0).Twice()
647	mockOverlay.EXPECT().SetChild(mockWebViewWidget).Once()
648
649	err := wv.SetWebViewWidget(entity.PaneID("pane-1"), mockWebViewWidget)
650
651	require.NoError(t, err)
652	assert.Equal(t, entity.PaneID("pane-1"), attachedPane)
653}

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