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}