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