v0.20.0 c741a39

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 setupWorkspaceViewBase(t *testing.T, mockFactory *mocks.MockWidgetFactory) (context.Context, *mocks.MockBoxWidget) {
18	ctx := context.Background()
19	mockContainer := mocks.NewMockBoxWidget(t)
20	mockOverlay := mocks.NewMockOverlayWidget(t)
21
22	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
23	mockContainer.EXPECT().SetHexpand(true).Once()
24	mockContainer.EXPECT().SetVexpand(true).Once()
25	mockContainer.EXPECT().SetVisible(true).Once()
26
27	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
28	mockOverlay.EXPECT().SetHexpand(true).Once()
29	mockOverlay.EXPECT().SetVexpand(true).Once()
30	mockOverlay.EXPECT().SetChild(mockContainer).Once()
31	mockOverlay.EXPECT().SetVisible(true).Once()
32
33	return ctx, mockContainer
34}
35
36func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory) (*mocks.MockOverlayWidget, *mocks.MockBoxWidget) {
37	mockOverlay := mocks.NewMockOverlayWidget(t)
38	mockBorderBox := mocks.NewMockBoxWidget(t)
39
40	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
41	mockOverlay.EXPECT().SetHexpand(true).Once()
42	mockOverlay.EXPECT().SetVexpand(true).Once()
43	mockOverlay.EXPECT().SetVisible(true).Once()
44
45	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
46	mockBorderBox.EXPECT().SetCanFocus(false).Once()
47	mockBorderBox.EXPECT().SetCanTarget(false).Once()
48	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
49	mockBorderBox.EXPECT().SetHexpand(true).Once()
50	mockBorderBox.EXPECT().SetVexpand(true).Once()
51	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
52	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
53	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
54
55	// GtkWidget is called when attaching hover handler - return nil for tests
56	mockOverlay.EXPECT().GtkWidget().Return(nil).Once()
57
58	return mockOverlay, mockBorderBox
59}
60
61func setupStackedLeafMocks(
62	t *testing.T,
63	mockFactory *mocks.MockWidgetFactory,
64	container *mocks.MockOverlayWidget,
65) *mocks.MockBoxWidget {
66	mockStackBox := mocks.NewMockBoxWidget(t)
67	mockTitleBar := mocks.NewMockBoxWidget(t)
68	mockFavicon := mocks.NewMockImageWidget(t)
69	mockLabel := mocks.NewMockLabelWidget(t)
70	mockButton := mocks.NewMockButtonWidget(t)
71
72	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockStackBox).Once()
73	mockStackBox.EXPECT().SetHexpand(true).Once()
74	mockStackBox.EXPECT().SetVexpand(true).Once()
75
76	mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
77	mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
78	mockTitleBar.EXPECT().SetVexpand(false).Once()
79
80	mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
81	mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
82	mockFavicon.EXPECT().SetPixelSize(16).Once()
83	mockTitleBar.EXPECT().Append(mockFavicon).Once()
84
85	mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once()
86	mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
87	mockLabel.EXPECT().SetMaxWidthChars(30).Once()
88	mockLabel.EXPECT().SetHexpand(true).Once()
89	mockLabel.EXPECT().SetXalign(float32(0.0)).Once()
90	mockTitleBar.EXPECT().Append(mockLabel).Once()
91
92	mockFactory.EXPECT().NewButton().Return(mockButton).Once()
93	mockButton.EXPECT().SetChild(mockTitleBar).Once()
94	mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once()
95	mockButton.EXPECT().SetFocusOnClick(false).Once()
96	mockButton.EXPECT().SetVexpand(false).Once()
97	mockButton.EXPECT().SetHexpand(true).Once()
98	mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
99
100	mockStackBox.EXPECT().Append(mockButton).Once()
101	mockStackBox.EXPECT().Append(container).Once()
102
103	mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
104	container.EXPECT().SetVisible(true).Once()
105	mockTitleBar.EXPECT().AddCssClass("active").Once()
106
107	return mockStackBox
108}
109
110func TestNewWorkspaceView_CreatesEmptyContainer(t *testing.T) {
111	// Arrange
112	mockFactory := mocks.NewMockWidgetFactory(t)
113	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
114
115	// Act
116	wv := component.NewWorkspaceView(ctx, mockFactory)
117
118	// Assert
119	require.NotNil(t, wv)
120	assert.Equal(t, 0, wv.PaneCount())
121	assert.Equal(t, mockBox, wv.Container())
122}
123
124func TestSetWorkspace_NilWorkspace_ReturnsError(t *testing.T) {
125	// Arrange
126	mockFactory := mocks.NewMockWidgetFactory(t)
127	ctx, _ := setupWorkspaceViewBase(t, mockFactory)
128
129	wv := component.NewWorkspaceView(ctx, mockFactory)
130
131	// Act
132	err := wv.SetWorkspace(ctx, nil)
133
134	// Assert
135	assert.ErrorIs(t, err, component.ErrNilWorkspace)
136}
137
138func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) {
139	// Arrange
140	mockFactory := mocks.NewMockWidgetFactory(t)
141	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
142
143	wv := component.NewWorkspaceView(ctx, mockFactory)
144
145	mockOverlay, mockBorderBox := setupWorkspacePaneViewMocks(t, mockFactory)
146	mockStackBox := setupStackedLeafMocks(t, mockFactory, mockOverlay)
147
148	mockStackBox.EXPECT().SetVisible(true).Once()
149	mockBox.EXPECT().Append(mockStackBox).Once()
150	mockBox.EXPECT().AddCssClass("single-pane").Once()
151	mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
152
153	pane := entity.NewPane(entity.PaneID("pane-1"))
154	node := &entity.PaneNode{ID: "node-1", Pane: pane}
155
156	ws := &entity.Workspace{
157		ID:           "ws-1",
158		Root:         node,
159		ActivePaneID: pane.ID,
160	}
161
162	// Act
163	err := wv.SetWorkspace(ctx, ws)
164
165	// Assert
166	require.NoError(t, err)
167	assert.Equal(t, 1, wv.PaneCount())
168	assert.Equal(t, pane.ID, wv.GetActivePaneID())
169	assert.NotNil(t, wv.GetPaneView(pane.ID))
170}
171
172func TestSetActivePaneID_UpdatesStyling(t *testing.T) {
173	// Arrange
174	mockFactory := mocks.NewMockWidgetFactory(t)
175	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
176
177	wv := component.NewWorkspaceView(ctx, mockFactory)
178
179	// Setup two panes
180	pane1 := entity.NewPane(entity.PaneID("pane-1"))
181	pane2 := entity.NewPane(entity.PaneID("pane-2"))
182	node1 := &entity.PaneNode{ID: "node-1", Pane: pane1}
183	node2 := &entity.PaneNode{ID: "node-2", Pane: pane2}
184
185	// Create mock widgets for split
186	mockPaned := mocks.NewMockPanedWidget(t)
187	mockOverlay1, mockBorderBox1 := setupWorkspacePaneViewMocks(t, mockFactory)
188	mockStackBox1 := setupStackedLeafMocks(t, mockFactory, mockOverlay1)
189	mockOverlay2, mockBorderBox2 := setupWorkspacePaneViewMocks(t, mockFactory)
190	mockStackBox2 := setupStackedLeafMocks(t, mockFactory, mockOverlay2)
191
192	// Split view creation
193	mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
194	mockPaned.EXPECT().SetResizeStartChild(true).Once()
195	mockPaned.EXPECT().SetResizeEndChild(true).Once()
196	mockPaned.EXPECT().SetVisible(true).Twice()
197	mockStackBox1.EXPECT().SetVisible(true).Once()
198	mockStackBox2.EXPECT().SetVisible(true).Once()
199	mockPaned.EXPECT().SetStartChild(mockStackBox1).Once()
200	mockPaned.EXPECT().SetEndChild(mockStackBox2).Once()
201	mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
202	mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once()
203	mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
204
205	mockBox.EXPECT().Append(mockPaned).Once()
206
207	// Initial active pane is pane-1
208	mockBox.EXPECT().RemoveCssClass("single-pane").Once()
209	mockBorderBox1.EXPECT().AddCssClass("pane-active").Once()
210
211	splitNode := &entity.PaneNode{
212		ID:         "split",
213		SplitDir:   entity.SplitHorizontal,
214		SplitRatio: 0.5,
215		Children:   []*entity.PaneNode{node1, node2},
216	}
217
218	ws := &entity.Workspace{
219		ID:           "ws-1",
220		Root:         splitNode,
221		ActivePaneID: pane1.ID,
222	}
223
224	err := wv.SetWorkspace(ctx, ws)
225	require.NoError(t, err)
226
227	// Now change active pane
228	mockBorderBox1.EXPECT().RemoveCssClass("pane-active").Once()
229	mockBorderBox2.EXPECT().AddCssClass("pane-active").Once()
230
231	// Act
232	err = wv.SetActivePaneID(pane2.ID)
233
234	// Assert
235	require.NoError(t, err)
236	assert.Equal(t, pane2.ID, wv.GetActivePaneID())
237}
238
239func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) {
240	// Arrange
241	mockFactory := mocks.NewMockWidgetFactory(t)
242	ctx, _ := setupWorkspaceViewBase(t, mockFactory)
243
244	wv := component.NewWorkspaceView(ctx, mockFactory)
245
246	// Act
247	err := wv.SetActivePaneID(entity.PaneID("non-existent"))
248
249	// Assert
250	assert.ErrorIs(t, err, component.ErrPaneNotFound)
251}

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