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 := >k.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}