1package layout_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/ui/layout"
12 "github.com/bnema/dumber/internal/ui/layout/mocks"
13)
14
15// setupMockFactory creates a factory that returns configured mocks for stacked view tests.
16func setupMockFactory(t *testing.T) (*mocks.MockWidgetFactory, *mocks.MockBoxWidget) {
17 mockFactory := mocks.NewMockWidgetFactory(t)
18 mockBox := mocks.NewMockBoxWidget(t)
19
20 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBox).Once()
21 mockBox.EXPECT().SetHexpand(true).Once()
22 mockBox.EXPECT().SetVexpand(true).Once()
23
24 return mockFactory, mockBox
25}
26
27// setupPaneMocks creates mocks needed for AddPane
28// Note: The stacked view now uses GestureClick on the titleBar directly instead of wrapping it in a button.
29// The GestureClick is added via AddController which we mock to accept any EventController.
30func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox *mocks.MockBoxWidget) (
31 *mocks.MockBoxWidget, *mocks.MockImageWidget, *mocks.MockLabelWidget, *mocks.MockWidget,
32) {
33 mockTitleBar := mocks.NewMockBoxWidget(t)
34 mockFavicon := mocks.NewMockImageWidget(t)
35 mockLabel := mocks.NewMockLabelWidget(t)
36 mockCloseButton := mocks.NewMockButtonWidget(t)
37 mockContainer := mocks.NewMockWidget(t)
38
39 // Title bar creation - now directly used without button wrapper
40 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
41 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
42 mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
43 mockTitleBar.EXPECT().SetVexpand(false).Once()
44 mockTitleBar.EXPECT().SetHexpand(true).Once()
45
46 // Favicon
47 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
48 mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
49 mockFavicon.EXPECT().SetPixelSize(16).Once()
50 mockTitleBar.EXPECT().Append(mockFavicon).Once()
51
52 // Label
53 mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once()
54 mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
55 mockLabel.EXPECT().SetMaxWidthChars(30).Once()
56 mockLabel.EXPECT().SetHexpand(true).Once()
57 mockLabel.EXPECT().SetXalign(float32(0.0)).Once()
58 mockTitleBar.EXPECT().Append(mockLabel).Once()
59
60 // Close button (uses SetIconName directly instead of child image)
61 mockFactory.EXPECT().NewButton().Return(mockCloseButton).Once()
62 mockCloseButton.EXPECT().SetIconName("window-close-symbolic").Once()
63 mockCloseButton.EXPECT().AddCssClass("stacked-pane-close-button").Once()
64 mockCloseButton.EXPECT().SetFocusOnClick(false).Once()
65 mockCloseButton.EXPECT().SetVexpand(false).Once()
66 mockCloseButton.EXPECT().SetHexpand(false).Once()
67 mockTitleBar.EXPECT().Append(mockCloseButton).Once()
68
69 // GestureClick is added to titleBar via AddController
70 mockTitleBar.EXPECT().AddController(mock.Anything).Once()
71
72 // Close button click handler
73 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint(2)).Once()
74
75 // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
76 mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
77
78 // Adding to main box - now titleBar is added directly (no button wrapper)
79 mockBox.EXPECT().Append(mockTitleBar).Once()
80 mockBox.EXPECT().Append(mockContainer).Once()
81
82 return mockTitleBar, mockFavicon, mockLabel, mockContainer
83}
84
85func TestNewStackedView_EmptyStack(t *testing.T) {
86 // Arrange
87 mockFactory, _ := setupMockFactory(t)
88
89 // Act
90 sv := layout.NewStackedView(mockFactory)
91
92 // Assert
93 require.NotNil(t, sv)
94 assert.Equal(t, 0, sv.Count())
95 assert.Equal(t, -1, sv.ActiveIndex())
96}
97
98func TestAddPane_SinglePane_BecomesActive(t *testing.T) {
99 // Arrange
100 ctx := context.Background()
101 mockFactory, mockBox := setupMockFactory(t)
102 mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
103 _ = mockFavicon
104 _ = mockLabel
105
106 // Visibility updates for active pane - titleBar is now directly in box, SetVisible called on it
107 mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
108 mockContainer.EXPECT().SetVisible(true).Once()
109 mockTitleBar.EXPECT().AddCssClass("active").Once()
110
111 sv := layout.NewStackedView(mockFactory)
112
113 // Act
114 index := sv.AddPane(ctx, "pane-1", "Test Page", "web-browser-symbolic", mockContainer)
115
116 // Assert
117 assert.Equal(t, 0, index)
118 assert.Equal(t, 1, sv.Count())
119 assert.Equal(t, 0, sv.ActiveIndex())
120}
121
122func TestAddPane_MultiplePanes_LastBecomesActive(t *testing.T) {
123 // Arrange
124 ctx := context.Background()
125 mockFactory, mockBox := setupMockFactory(t)
126
127 // First pane
128 mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
129 _ = mockFavicon1
130 _ = mockLabel1
131 mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
132 mockContainer1.EXPECT().SetVisible(true).Once()
133 mockTitleBar1.EXPECT().AddCssClass("active").Once()
134
135 // Second pane - first pane becomes inactive
136 mockTitleBar2, mockFavicon2, mockLabel2, mockContainer2 := setupPaneMocks(t, mockFactory, mockBox)
137 _ = mockFavicon2
138 _ = mockLabel2
139
140 // When second pane is added, update visibility for both
141 mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
142 mockContainer1.EXPECT().SetVisible(false).Once()
143 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
144
145 mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
146 mockContainer2.EXPECT().SetVisible(true).Once()
147 mockTitleBar2.EXPECT().AddCssClass("active").Once()
148
149 sv := layout.NewStackedView(mockFactory)
150
151 // Act
152 sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1)
153 index := sv.AddPane(ctx, "pane-2", "Page 2", "", mockContainer2)
154
155 // Assert
156 assert.Equal(t, 1, index)
157 assert.Equal(t, 2, sv.Count())
158 assert.Equal(t, 1, sv.ActiveIndex())
159}
160
161func TestRemovePane_MiddlePane(t *testing.T) {
162 // Arrange
163 ctx := context.Background()
164 mockFactory, mockBox := setupMockFactory(t)
165
166 // Setup 3 panes (simplified - just track the key behaviors)
167 containers := make([]*mocks.MockWidget, 3)
168 titleBars := make([]*mocks.MockBoxWidget, 3)
169
170 for i := range 3 {
171 titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
172 }
173
174 // Allow any visibility calls during setup and removal
175 for i := range 3 {
176 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
177 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
178 titleBars[i].EXPECT().AddCssClass("active").Maybe()
179 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
180 }
181
182 sv := layout.NewStackedView(mockFactory)
183 sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
184 sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
185 sv.AddPane(ctx, "pane-3", "Page 3", "", containers[2])
186
187 // Remove middle pane (index 1)
188 // The titleBar is now directly in the box (no button wrapper)
189 mockBox.EXPECT().Remove(titleBars[1]).Once()
190 mockBox.EXPECT().Remove(containers[1]).Once()
191
192 // Act
193 err := sv.RemovePane(ctx, 1)
194
195 // Assert
196 require.NoError(t, err)
197 assert.Equal(t, 2, sv.Count())
198}
199
200func TestRemovePane_LastPane_ReturnsError(t *testing.T) {
201 // Arrange
202 ctx := context.Background()
203 mockFactory, mockBox := setupMockFactory(t)
204 mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
205 _ = mockFavicon
206 _ = mockLabel
207
208 mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
209 mockContainer.EXPECT().SetVisible(true).Once()
210 mockTitleBar.EXPECT().AddCssClass("active").Once()
211
212 sv := layout.NewStackedView(mockFactory)
213 sv.AddPane(ctx, "pane-1", "Only Page", "", mockContainer)
214
215 // Act
216 err := sv.RemovePane(ctx, 0)
217
218 // Assert
219 require.ErrorIs(t, err, layout.ErrCannotRemoveLastPane)
220 assert.Equal(t, 1, sv.Count())
221}
222
223func TestRemovePane_EmptyStack_ReturnsError(t *testing.T) {
224 // Arrange
225 ctx := context.Background()
226 mockFactory, _ := setupMockFactory(t)
227 sv := layout.NewStackedView(mockFactory)
228
229 // Act
230 err := sv.RemovePane(ctx, 0)
231
232 // Assert
233 require.ErrorIs(t, err, layout.ErrStackEmpty)
234}
235
236func TestRemovePane_IndexOutOfBounds(t *testing.T) {
237 // Arrange
238 ctx := context.Background()
239 mockFactory, mockBox := setupMockFactory(t)
240 mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
241 _ = mockFavicon
242 _ = mockLabel
243
244 mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
245 mockContainer.EXPECT().SetVisible(true).Once()
246 mockTitleBar.EXPECT().AddCssClass("active").Once()
247
248 sv := layout.NewStackedView(mockFactory)
249 sv.AddPane(ctx, "pane-1", "Page", "", mockContainer)
250
251 // Act
252 err := sv.RemovePane(ctx, 5)
253
254 // Assert
255 require.ErrorIs(t, err, layout.ErrIndexOutOfBounds)
256}
257
258func TestSetActive_ValidIndex(t *testing.T) {
259 // Arrange
260 ctx := context.Background()
261 mockFactory, mockBox := setupMockFactory(t)
262
263 containers := make([]*mocks.MockWidget, 2)
264 titleBars := make([]*mocks.MockBoxWidget, 2)
265
266 for i := range 2 {
267 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
268 _ = mockFavicon
269 _ = mockLabel
270
271 titleBars[i] = titleBar
272 containers[i] = container
273
274 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
275 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
276 titleBars[i].EXPECT().AddCssClass("active").Maybe()
277 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
278 }
279
280 sv := layout.NewStackedView(mockFactory)
281 sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
282 sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
283
284 // Act - switch back to first pane
285 err := sv.SetActive(ctx, 0)
286
287 // Assert
288 require.NoError(t, err)
289 assert.Equal(t, 0, sv.ActiveIndex())
290}
291
292func TestSetActive_OutOfBounds(t *testing.T) {
293 // Arrange
294 ctx := context.Background()
295 mockFactory, mockBox := setupMockFactory(t)
296 mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
297 _ = mockFavicon
298 _ = mockLabel
299
300 mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
301 mockContainer.EXPECT().SetVisible(true).Once()
302 mockTitleBar.EXPECT().AddCssClass("active").Once()
303
304 sv := layout.NewStackedView(mockFactory)
305 sv.AddPane(ctx, "pane-1", "Page", "", mockContainer)
306
307 // Act
308 err := sv.SetActive(ctx, 5)
309
310 // Assert
311 require.ErrorIs(t, err, layout.ErrIndexOutOfBounds)
312}
313
314func TestSetActive_EmptyStack(t *testing.T) {
315 // Arrange
316 ctx := context.Background()
317 mockFactory, _ := setupMockFactory(t)
318 sv := layout.NewStackedView(mockFactory)
319
320 // Act
321 err := sv.SetActive(ctx, 0)
322
323 // Assert
324 require.ErrorIs(t, err, layout.ErrStackEmpty)
325}
326
327func TestUpdateTitle(t *testing.T) {
328 // Arrange
329 ctx := context.Background()
330 mockFactory, mockBox := setupMockFactory(t)
331 mockTitleBar, _, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
332
333 mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
334 mockContainer.EXPECT().SetVisible(true).Once()
335 mockTitleBar.EXPECT().AddCssClass("active").Once()
336
337 sv := layout.NewStackedView(mockFactory)
338 sv.AddPane(ctx, "pane-1", "Old Title", "", mockContainer)
339
340 mockLabel.EXPECT().SetText("New Title").Once()
341
342 // Act
343 err := sv.UpdateTitle(0, "New Title")
344
345 // Assert
346 require.NoError(t, err)
347}
348
349func TestUpdateTitle_InvalidIndex(t *testing.T) {
350 // Arrange
351 mockFactory, _ := setupMockFactory(t)
352 sv := layout.NewStackedView(mockFactory)
353
354 // Act
355 err := sv.UpdateTitle(0, "Title")
356
357 // Assert
358 require.ErrorIs(t, err, layout.ErrIndexOutOfBounds)
359}
360
361func TestUpdateFavicon(t *testing.T) {
362 // Arrange
363 ctx := context.Background()
364 mockFactory, mockBox := setupMockFactory(t)
365 mockTitleBar, mockFavicon, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
366
367 mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
368 mockContainer.EXPECT().SetVisible(true).Once()
369 mockTitleBar.EXPECT().AddCssClass("active").Once()
370
371 sv := layout.NewStackedView(mockFactory)
372 sv.AddPane(ctx, "pane-1", "Page", "", mockContainer)
373
374 mockFavicon.EXPECT().SetFromIconName("new-icon").Once()
375
376 // Act
377 err := sv.UpdateFavicon(0, "new-icon")
378
379 // Assert
380 require.NoError(t, err)
381}
382
383func TestGetContainer(t *testing.T) {
384 // Arrange
385 ctx := context.Background()
386 mockFactory, mockBox := setupMockFactory(t)
387 mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
388 _ = mockFavicon
389 _ = mockLabel
390
391 mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
392 mockContainer.EXPECT().SetVisible(true).Once()
393 mockTitleBar.EXPECT().AddCssClass("active").Once()
394
395 sv := layout.NewStackedView(mockFactory)
396 sv.AddPane(ctx, "pane-1", "Page", "", mockContainer)
397
398 // Act
399 container, err := sv.GetContainer(0)
400
401 // Assert
402 require.NoError(t, err)
403 assert.Equal(t, mockContainer, container)
404}
405
406func TestGetContainer_InvalidIndex(t *testing.T) {
407 // Arrange
408 mockFactory, _ := setupMockFactory(t)
409 sv := layout.NewStackedView(mockFactory)
410
411 // Act
412 container, err := sv.GetContainer(0)
413
414 // Assert
415 require.ErrorIs(t, err, layout.ErrIndexOutOfBounds)
416 assert.Nil(t, container)
417}
418
419func TestWidget_ReturnsBoxWidget(t *testing.T) {
420 // Arrange
421 mockFactory, mockBox := setupMockFactory(t)
422 sv := layout.NewStackedView(mockFactory)
423
424 // Act
425 widget := sv.Widget()
426
427 // Assert
428 assert.Equal(t, mockBox, widget)
429}
430
431func TestNavigateNext_WrapsAround(t *testing.T) {
432 // Arrange
433 ctx := context.Background()
434 mockFactory, mockBox := setupMockFactory(t)
435
436 containers := make([]*mocks.MockWidget, 2)
437 titleBars := make([]*mocks.MockBoxWidget, 2)
438
439 for i := range 2 {
440 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
441 _ = mockFavicon
442 _ = mockLabel
443
444 titleBars[i] = titleBar
445 containers[i] = container
446
447 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
448 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
449 titleBars[i].EXPECT().AddCssClass("active").Maybe()
450 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
451 }
452
453 sv := layout.NewStackedView(mockFactory)
454 sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
455 sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
456
457 // Currently at index 1 (last added)
458 assert.Equal(t, 1, sv.ActiveIndex())
459
460 // Act - navigate next should wrap to 0
461 err := sv.NavigateNext(ctx)
462
463 // Assert
464 require.NoError(t, err)
465 assert.Equal(t, 0, sv.ActiveIndex())
466}
467
468func TestNavigatePrevious_WrapsAround(t *testing.T) {
469 // Arrange
470 ctx := context.Background()
471 mockFactory, mockBox := setupMockFactory(t)
472
473 containers := make([]*mocks.MockWidget, 2)
474 titleBars := make([]*mocks.MockBoxWidget, 2)
475
476 for i := range 2 {
477 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
478 _ = mockFavicon
479 _ = mockLabel
480
481 titleBars[i] = titleBar
482 containers[i] = container
483
484 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
485 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
486 titleBars[i].EXPECT().AddCssClass("active").Maybe()
487 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
488 }
489
490 sv := layout.NewStackedView(mockFactory)
491 sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
492 sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
493
494 // Set to first pane
495 sv.SetActive(ctx, 0)
496
497 // Act - navigate previous should wrap to 1
498 err := sv.NavigatePrevious(ctx)
499
500 // Assert
501 require.NoError(t, err)
502 assert.Equal(t, 1, sv.ActiveIndex())
503}
504
505func TestNavigateNext_EmptyStack(t *testing.T) {
506 // Arrange
507 ctx := context.Background()
508 mockFactory, _ := setupMockFactory(t)
509 sv := layout.NewStackedView(mockFactory)
510
511 // Act
512 err := sv.NavigateNext(ctx)
513
514 // Assert
515 require.ErrorIs(t, err, layout.ErrStackEmpty)
516}
517
518func TestNavigatePrevious_EmptyStack(t *testing.T) {
519 // Arrange
520 ctx := context.Background()
521 mockFactory, _ := setupMockFactory(t)
522 sv := layout.NewStackedView(mockFactory)
523
524 // Act
525 err := sv.NavigatePrevious(ctx)
526
527 // Assert
528 require.ErrorIs(t, err, layout.ErrStackEmpty)
529}
530
531func TestSetOnActivate_Callback(t *testing.T) {
532 // Arrange
533 mockFactory, _ := setupMockFactory(t)
534 sv := layout.NewStackedView(mockFactory)
535
536 callback := func(index int) {
537 _ = index // Used to test callback registration
538 }
539
540 // Act
541 sv.SetOnActivate(callback)
542
543 // We can't easily test the callback being triggered since it requires
544 // simulating a button click. The test verifies the method doesn't panic.
545 // Integration tests would verify the actual callback invocation.
546}
547
548// setupInsertPaneMocks creates mocks needed for InsertPaneAfter with position-aware insertion.
549// Returns only the titleBar and container mocks that are needed by test assertions.
550func setupInsertPaneMocks(
551 t *testing.T,
552 mockFactory *mocks.MockWidgetFactory,
553 mockBox *mocks.MockBoxWidget,
554 siblingContainer layout.Widget,
555) (*mocks.MockBoxWidget, *mocks.MockWidget) {
556 mockTitleBar := mocks.NewMockBoxWidget(t)
557 mockFavicon := mocks.NewMockImageWidget(t)
558 mockLabel := mocks.NewMockLabelWidget(t)
559 mockCloseButton := mocks.NewMockButtonWidget(t)
560 mockContainer := mocks.NewMockWidget(t)
561
562 // Title bar creation - now directly used without button wrapper
563 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
564 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
565 mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
566 mockTitleBar.EXPECT().SetVexpand(false).Once()
567 mockTitleBar.EXPECT().SetHexpand(true).Once()
568
569 // Favicon
570 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
571 mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
572 mockFavicon.EXPECT().SetPixelSize(16).Once()
573 mockTitleBar.EXPECT().Append(mockFavicon).Once()
574
575 // Label
576 mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once()
577 mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
578 mockLabel.EXPECT().SetMaxWidthChars(30).Once()
579 mockLabel.EXPECT().SetHexpand(true).Once()
580 mockLabel.EXPECT().SetXalign(float32(0.0)).Once()
581 mockTitleBar.EXPECT().Append(mockLabel).Once()
582
583 // Close button (uses SetIconName directly instead of child image)
584 mockFactory.EXPECT().NewButton().Return(mockCloseButton).Once()
585 mockCloseButton.EXPECT().SetIconName("window-close-symbolic").Once()
586 mockCloseButton.EXPECT().AddCssClass("stacked-pane-close-button").Once()
587 mockCloseButton.EXPECT().SetFocusOnClick(false).Once()
588 mockCloseButton.EXPECT().SetVexpand(false).Once()
589 mockCloseButton.EXPECT().SetHexpand(false).Once()
590 mockTitleBar.EXPECT().Append(mockCloseButton).Once()
591
592 // GestureClick is added to titleBar via AddController
593 mockTitleBar.EXPECT().AddController(mock.Anything).Once()
594
595 // Close button click handler
596 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint(2)).Once()
597
598 // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
599 mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
600
601 // Position-aware insertion using InsertChildAfter - now using titleBar directly
602 if siblingContainer != nil {
603 mockBox.EXPECT().InsertChildAfter(mockTitleBar, siblingContainer).Once()
604 mockBox.EXPECT().InsertChildAfter(mockContainer, mockTitleBar).Once()
605 } else {
606 // Insert at beginning using Prepend
607 mockBox.EXPECT().Prepend(mockContainer).Once()
608 mockBox.EXPECT().Prepend(mockTitleBar).Once()
609 }
610
611 return mockTitleBar, mockContainer
612}
613
614func TestInsertPaneAfter_AtBeginning(t *testing.T) {
615 // Arrange
616 ctx := context.Background()
617 mockFactory, mockBox := setupMockFactory(t)
618
619 // First pane - appended normally
620 mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
621 _ = mockFavicon1
622 _ = mockLabel1
623 mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
624 mockContainer1.EXPECT().SetVisible(true).Once()
625 mockTitleBar1.EXPECT().AddCssClass("active").Once()
626
627 sv := layout.NewStackedView(mockFactory)
628 sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1)
629
630 // Second pane - inserted at beginning (afterIndex=-1)
631 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, nil)
632
633 // Visibility updates
634 mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
635 mockContainer1.EXPECT().SetVisible(false).Once()
636 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
637
638 mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
639 mockContainer2.EXPECT().SetVisible(true).Once()
640 mockTitleBar2.EXPECT().AddCssClass("active").Once()
641
642 // Act
643 index := sv.InsertPaneAfter(ctx, -1, "pane-0", "Page 0", "", mockContainer2)
644
645 // Assert
646 assert.Equal(t, 0, index)
647 assert.Equal(t, 2, sv.Count())
648 assert.Equal(t, 0, sv.ActiveIndex())
649}
650
651func TestInsertPaneAfter_InMiddle(t *testing.T) {
652 // Arrange
653 ctx := context.Background()
654 mockFactory, mockBox := setupMockFactory(t)
655
656 // Setup 2 initial panes
657 containers := make([]*mocks.MockWidget, 2)
658 titleBars := make([]*mocks.MockBoxWidget, 2)
659
660 for i := range 2 {
661 titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
662 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
663 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
664 titleBars[i].EXPECT().AddCssClass("active").Maybe()
665 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
666 }
667
668 sv := layout.NewStackedView(mockFactory)
669 sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0])
670 sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1])
671
672 // Set active to first pane
673 require.NoError(t, sv.SetActive(ctx, 0))
674
675 // Insert new pane after index 0 (should become index 1)
676 mockTitleBar3, mockContainer3 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[0])
677 mockTitleBar3.EXPECT().SetVisible(mock.Anything).Maybe()
678 mockContainer3.EXPECT().SetVisible(mock.Anything).Maybe()
679 mockTitleBar3.EXPECT().AddCssClass("active").Maybe()
680 mockTitleBar3.EXPECT().RemoveCssClass("active").Maybe()
681
682 // Act
683 index := sv.InsertPaneAfter(ctx, 0, "pane-1.5", "Page 1.5", "", mockContainer3)
684
685 // Assert
686 assert.Equal(t, 1, index)
687 assert.Equal(t, 3, sv.Count())
688 assert.Equal(t, 1, sv.ActiveIndex()) // New pane becomes active
689}
690
691func TestInsertPaneAfter_AtEnd(t *testing.T) {
692 // Arrange
693 ctx := context.Background()
694 mockFactory, mockBox := setupMockFactory(t)
695
696 // First pane
697 mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
698 _ = mockFavicon1
699 _ = mockLabel1
700 mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
701 mockContainer1.EXPECT().SetVisible(true).Once()
702 mockTitleBar1.EXPECT().AddCssClass("active").Once()
703
704 sv := layout.NewStackedView(mockFactory)
705 sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1)
706
707 // Insert after last pane (afterIndex=0, becomes index 1)
708 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)
709
710 mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
711 mockContainer1.EXPECT().SetVisible(false).Once()
712 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
713
714 mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
715 mockContainer2.EXPECT().SetVisible(true).Once()
716 mockTitleBar2.EXPECT().AddCssClass("active").Once()
717
718 // Act
719 index := sv.InsertPaneAfter(ctx, 0, "pane-2", "Page 2", "", mockContainer2)
720
721 // Assert
722 assert.Equal(t, 1, index)
723 assert.Equal(t, 2, sv.Count())
724 assert.Equal(t, 1, sv.ActiveIndex())
725}
726
727func TestInsertPaneAfter_MaintainsOrder(t *testing.T) {
728 // Arrange
729 ctx := context.Background()
730 mockFactory, mockBox := setupMockFactory(t)
731
732 // Setup 3 initial panes: A, B, C
733 containers := make([]*mocks.MockWidget, 3)
734 titleBars := make([]*mocks.MockBoxWidget, 3)
735
736 for i := range 3 {
737 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
738 _ = mockFavicon
739 _ = mockLabel
740
741 titleBars[i] = titleBar
742 containers[i] = container
743
744 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
745 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
746 titleBars[i].EXPECT().AddCssClass("active").Maybe()
747 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
748 }
749
750 sv := layout.NewStackedView(mockFactory)
751 sv.AddPane(ctx, "pane-a", "A", "", containers[0]) // index 0
752 sv.AddPane(ctx, "pane-b", "B", "", containers[1]) // index 1
753 sv.AddPane(ctx, "pane-c", "C", "", containers[2]) // index 2
754
755 // Navigate to pane B (index 1)
756 require.NoError(t, sv.SetActive(ctx, 1))
757
758 // Insert D after B (should be at index 2, C moves to index 3)
759 mockTitleBar4, mockContainer4 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[1])
760 mockTitleBar4.EXPECT().SetVisible(mock.Anything).Maybe()
761 mockContainer4.EXPECT().SetVisible(mock.Anything).Maybe()
762 mockTitleBar4.EXPECT().AddCssClass("active").Maybe()
763 mockTitleBar4.EXPECT().RemoveCssClass("active").Maybe()
764
765 // Act
766 index := sv.InsertPaneAfter(ctx, 1, "pane-d", "D", "", mockContainer4)
767
768 // Assert: Order should be A(0), B(1), D(2), C(3)
769 assert.Equal(t, 2, index)
770 assert.Equal(t, 4, sv.Count())
771 assert.Equal(t, 2, sv.ActiveIndex()) // D is now active
772
773 // Verify we can still get containers at expected indices
774 containerA, _ := sv.GetContainer(0)
775 containerB, _ := sv.GetContainer(1)
776 containerD, _ := sv.GetContainer(2)
777 containerC, _ := sv.GetContainer(3)
778
779 assert.Equal(t, containers[0], containerA)
780 assert.Equal(t, containers[1], containerB)
781 assert.Equal(t, mockContainer4, containerD)
782 assert.Equal(t, containers[2], containerC)
783}
784
785func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) {
786 // Arrange
787 ctx := context.Background()
788 mockFactory, mockBox := setupMockFactory(t)
789
790 // First pane
791 mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
792 _ = mockFavicon1
793 _ = mockLabel1
794 mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
795 mockContainer1.EXPECT().SetVisible(true).Once()
796 mockTitleBar1.EXPECT().AddCssClass("active").Once()
797
798 sv := layout.NewStackedView(mockFactory)
799 sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1)
800
801 // Try to insert at invalid index (100) - should clamp to end
802 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)
803
804 mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
805 mockContainer1.EXPECT().SetVisible(false).Once()
806 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
807
808 mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
809 mockContainer2.EXPECT().SetVisible(true).Once()
810 mockTitleBar2.EXPECT().AddCssClass("active").Once()
811
812 // Act - afterIndex=100 should be clamped to 0 (last valid index)
813 index := sv.InsertPaneAfter(ctx, 100, "pane-2", "Page 2", "", mockContainer2)
814
815 // Assert
816 assert.Equal(t, 1, index)
817 assert.Equal(t, 2, sv.Count())
818}