v0.21.0 73bf644

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

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