v0.22.0 a85da87

workspace_view_test.go RawHistoryFolder

1package component_test
2
3import (
4	"context"
5	"testing"
6
7	"github.com/stretchr/testify/assert"
8	"github.com/stretchr/testify/mock"
9	"github.com/stretchr/testify/require"
10
11	"github.com/bnema/dumber/internal/domain/entity"
12	"github.com/bnema/dumber/internal/ui/component"
13	"github.com/bnema/dumber/internal/ui/layout"
14	"github.com/bnema/dumber/internal/ui/layout/mocks"
15)
16
17func setupLoadingSkeletonMocksWorkspace(
18	mockFactory *mocks.MockWidgetFactory,
19	mockOverlay *mocks.MockOverlayWidget,
20	mockLoadingContainer *mocks.MockBoxWidget,
21	mockLoadingContent *mocks.MockBoxWidget,
22	mockLoadingSpinner *mocks.MockSpinnerWidget,
23	mockLoadingLogo *mocks.MockImageWidget,
24) {
25	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
26	mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
27	mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()
28	mockLoadingContainer.EXPECT().SetHalign(mock.Anything).Maybe()
29	mockLoadingContainer.EXPECT().SetValign(mock.Anything).Maybe()
30	mockLoadingContainer.EXPECT().SetCanFocus(false).Maybe()
31	mockLoadingContainer.EXPECT().SetCanTarget(false).Maybe()
32	mockLoadingContainer.EXPECT().AddCssClass("loading-skeleton").Maybe()
33	mockLoadingContainer.EXPECT().SetVisible(true).Maybe()
34
35	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 6).Return(mockLoadingContent).Once()
36	mockLoadingContent.EXPECT().SetHalign(mock.Anything).Maybe()
37	mockLoadingContent.EXPECT().SetValign(mock.Anything).Maybe()
38	mockLoadingContent.EXPECT().SetCanFocus(false).Maybe()
39	mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()
40	mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()
41
42	mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()
43	mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()
44	mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()
45	mockLoadingSpinner.EXPECT().SetCanFocus(false).Maybe()
46	mockLoadingSpinner.EXPECT().SetCanTarget(false).Maybe()
47	mockLoadingSpinner.EXPECT().SetSizeRequest(32, 32).Maybe()
48	mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
49	mockLoadingSpinner.EXPECT().Start().Maybe()
50
51	mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
52	mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
53	mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
54	mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
55	mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
56	mockLoadingLogo.EXPECT().SetSizeRequest(512, 512).Maybe()
57	mockLoadingLogo.EXPECT().SetPixelSize(512).Maybe()
58	mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
59	mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
60
61	mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
62	mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
63	mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
64
65	mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
66	mockOverlay.EXPECT().SetClipOverlay(mockLoadingContainer, false).Once()
67	mockOverlay.EXPECT().SetMeasureOverlay(mockLoadingContainer, false).Once()
68}
69
70func setupWorkspaceViewBase(t *testing.T, mockFactory *mocks.MockWidgetFactory) (context.Context, *mocks.MockBoxWidget) {
71	ctx := context.Background()
72	mockContainer := mocks.NewMockBoxWidget(t)
73	mockOverlay := mocks.NewMockOverlayWidget(t)
74
75	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
76	mockContainer.EXPECT().SetHexpand(true).Once()
77	mockContainer.EXPECT().SetVexpand(true).Once()
78	mockContainer.EXPECT().SetVisible(true).Once()
79
80	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
81	mockOverlay.EXPECT().SetHexpand(true).Once()
82	mockOverlay.EXPECT().SetVexpand(true).Once()
83	mockOverlay.EXPECT().SetChild(mockContainer).Once()
84	mockOverlay.EXPECT().SetVisible(true).Once()
85
86	return ctx, mockContainer
87}
88
89func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory) (*mocks.MockOverlayWidget, *mocks.MockBoxWidget) {
90	mockOverlay := mocks.NewMockOverlayWidget(t)
91	mockBorderBox := mocks.NewMockBoxWidget(t)
92
93	mockLoadingContainer := mocks.NewMockBoxWidget(t)
94	mockLoadingContent := mocks.NewMockBoxWidget(t)
95	mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
96	mockLoadingLogo := mocks.NewMockImageWidget(t)
97
98	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
99	mockOverlay.EXPECT().SetHexpand(true).Once()
100	mockOverlay.EXPECT().SetVexpand(true).Once()
101	mockOverlay.EXPECT().SetVisible(true).Once()
102	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()
103
104	setupLoadingSkeletonMocksWorkspace(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
105
106	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
107	mockBorderBox.EXPECT().SetCanFocus(false).Once()
108	mockBorderBox.EXPECT().SetCanTarget(false).Once()
109	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
110	mockBorderBox.EXPECT().SetHexpand(true).Once()
111	mockBorderBox.EXPECT().SetVexpand(true).Once()
112	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
113	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
114	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
115
116	// GtkWidget is called when attaching hover handler - return nil for tests
117	mockOverlay.EXPECT().GtkWidget().Return(nil).Once()
118
119	return mockOverlay, mockBorderBox
120}
121
122func setupStackedLeafMocks(
123	t *testing.T,
124	mockFactory *mocks.MockWidgetFactory,
125	container *mocks.MockOverlayWidget,
126) *mocks.MockBoxWidget {
127	mockStackBox := mocks.NewMockBoxWidget(t)
128	mockTitleBar := mocks.NewMockBoxWidget(t)
129	mockFavicon := mocks.NewMockImageWidget(t)
130	mockLabel := mocks.NewMockLabelWidget(t)
131	mockButton := mocks.NewMockButtonWidget(t)
132
133	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockStackBox).Once()
134	mockStackBox.EXPECT().SetHexpand(true).Once()
135	mockStackBox.EXPECT().SetVexpand(true).Once()
136
137	mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
138	mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
139	mockTitleBar.EXPECT().SetVexpand(false).Once()
140
141	mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
142	mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
143	mockFavicon.EXPECT().SetPixelSize(16).Once()
144	mockTitleBar.EXPECT().Append(mockFavicon).Once()
145
146	mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once()
147	mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
148	mockLabel.EXPECT().SetMaxWidthChars(30).Once()
149	mockLabel.EXPECT().SetHexpand(true).Once()
150	mockLabel.EXPECT().SetXalign(float32(0.0)).Once()
151	mockTitleBar.EXPECT().Append(mockLabel).Once()
152
153	mockFactory.EXPECT().NewButton().Return(mockButton).Once()
154	mockButton.EXPECT().SetChild(mockTitleBar).Once()
155	mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once()
156	mockButton.EXPECT().SetFocusOnClick(false).Once()
157	mockButton.EXPECT().SetVexpand(false).Once()
158	mockButton.EXPECT().SetHexpand(true).Once()
159	mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
160
161	mockStackBox.EXPECT().Append(mockButton).Once()
162	mockStackBox.EXPECT().Append(container).Once()
163
164	mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
165	container.EXPECT().SetVisible(true).Once()
166	mockTitleBar.EXPECT().AddCssClass("active").Once()
167
168	return mockStackBox
169}
170
171func TestNewWorkspaceView_CreatesEmptyContainer(t *testing.T) {
172	// Arrange
173	mockFactory := mocks.NewMockWidgetFactory(t)
174	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
175
176	// Act
177	wv := component.NewWorkspaceView(ctx, mockFactory)
178
179	// Assert
180	require.NotNil(t, wv)
181	assert.Equal(t, 0, wv.PaneCount())
182	assert.Equal(t, mockBox, wv.Container())
183}
184
185func TestSetWorkspace_NilWorkspace_ReturnsError(t *testing.T) {
186	// Arrange
187	mockFactory := mocks.NewMockWidgetFactory(t)
188	ctx, _ := setupWorkspaceViewBase(t, mockFactory)
189
190	wv := component.NewWorkspaceView(ctx, mockFactory)
191
192	// Act
193	err := wv.SetWorkspace(ctx, nil)
194
195	// Assert
196	assert.ErrorIs(t, err, component.ErrNilWorkspace)
197}
198
199func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) {
200	// Arrange
201	mockFactory := mocks.NewMockWidgetFactory(t)
202	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
203
204	wv := component.NewWorkspaceView(ctx, mockFactory)
205
206	mockOverlay, mockBorderBox := setupWorkspacePaneViewMocks(t, mockFactory)
207	mockStackBox := setupStackedLeafMocks(t, mockFactory, mockOverlay)
208
209	mockStackBox.EXPECT().SetVisible(true).Once()
210	mockBox.EXPECT().Append(mockStackBox).Once()
211	mockBox.EXPECT().AddCssClass("single-pane").Once()
212	mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
213
214	pane := entity.NewPane(entity.PaneID("pane-1"))
215	node := &entity.PaneNode{ID: "node-1", Pane: pane}
216
217	ws := &entity.Workspace{
218		ID:           "ws-1",
219		Root:         node,
220		ActivePaneID: pane.ID,
221	}
222
223	// Act
224	err := wv.SetWorkspace(ctx, ws)
225
226	// Assert
227	require.NoError(t, err)
228	assert.Equal(t, 1, wv.PaneCount())
229	assert.Equal(t, pane.ID, wv.GetActivePaneID())
230	assert.NotNil(t, wv.GetPaneView(pane.ID))
231}
232
233func TestSetActivePaneID_UpdatesStyling(t *testing.T) {
234	// Arrange
235	mockFactory := mocks.NewMockWidgetFactory(t)
236	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
237
238	wv := component.NewWorkspaceView(ctx, mockFactory)
239
240	// Setup two panes
241	pane1 := entity.NewPane(entity.PaneID("pane-1"))
242	pane2 := entity.NewPane(entity.PaneID("pane-2"))
243	node1 := &entity.PaneNode{ID: "node-1", Pane: pane1}
244	node2 := &entity.PaneNode{ID: "node-2", Pane: pane2}
245
246	// Create mock widgets for split
247	mockPaned := mocks.NewMockPanedWidget(t)
248	mockOverlay1, mockBorderBox1 := setupWorkspacePaneViewMocks(t, mockFactory)
249	mockStackBox1 := setupStackedLeafMocks(t, mockFactory, mockOverlay1)
250	mockOverlay2, mockBorderBox2 := setupWorkspacePaneViewMocks(t, mockFactory)
251	mockStackBox2 := setupStackedLeafMocks(t, mockFactory, mockOverlay2)
252
253	// Split view creation
254	mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
255	mockPaned.EXPECT().SetResizeStartChild(true).Once()
256	mockPaned.EXPECT().SetResizeEndChild(true).Once()
257	mockPaned.EXPECT().SetVisible(true).Twice()
258	mockStackBox1.EXPECT().SetVisible(true).Once()
259	mockStackBox2.EXPECT().SetVisible(true).Once()
260	mockPaned.EXPECT().SetStartChild(mockStackBox1).Once()
261	mockPaned.EXPECT().SetEndChild(mockStackBox2).Once()
262	mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once()
263	mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
264	mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once()
265	mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
266
267	mockBox.EXPECT().Append(mockPaned).Once()
268
269	// Initial active pane is pane-1
270	mockBox.EXPECT().RemoveCssClass("single-pane").Once()
271	mockBorderBox1.EXPECT().AddCssClass("pane-active").Once()
272
273	splitNode := &entity.PaneNode{
274		ID:         "split",
275		SplitDir:   entity.SplitHorizontal,
276		SplitRatio: 0.5,
277		Children:   []*entity.PaneNode{node1, node2},
278	}
279
280	ws := &entity.Workspace{
281		ID:           "ws-1",
282		Root:         splitNode,
283		ActivePaneID: pane1.ID,
284	}
285
286	err := wv.SetWorkspace(ctx, ws)
287	require.NoError(t, err)
288
289	// Now change active pane
290	mockBorderBox1.EXPECT().RemoveCssClass("pane-active").Once()
291	mockBorderBox2.EXPECT().AddCssClass("pane-active").Once()
292
293	// Act
294	err = wv.SetActivePaneID(pane2.ID)
295
296	// Assert
297	require.NoError(t, err)
298	assert.Equal(t, pane2.ID, wv.GetActivePaneID())
299}
300
301func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) {
302	// Arrange
303	mockFactory := mocks.NewMockWidgetFactory(t)
304	ctx, _ := setupWorkspaceViewBase(t, mockFactory)
305
306	wv := component.NewWorkspaceView(ctx, mockFactory)
307
308	// Act
309	err := wv.SetActivePaneID(entity.PaneID("non-existent"))
310
311	// Assert
312	assert.ErrorIs(t, err, component.ErrPaneNotFound)
313}

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