package layout_test import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/bnema/dumber/internal/ui/layout" "github.com/bnema/dumber/internal/ui/layout/mocks" ) // setupMockFactory creates a factory that returns configured mocks for stacked view tests. func setupMockFactory(t *testing.T) (*mocks.MockWidgetFactory, *mocks.MockBoxWidget) { mockFactory := mocks.NewMockWidgetFactory(t) mockBox := mocks.NewMockBoxWidget(t) mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBox).Once() mockBox.EXPECT().SetHexpand(true).Once() mockBox.EXPECT().SetVexpand(true).Once() return mockFactory, mockBox } // setupPaneMocks creates mocks needed for AddPane func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox *mocks.MockBoxWidget) ( *mocks.MockBoxWidget, *mocks.MockImageWidget, *mocks.MockLabelWidget, *mocks.MockButtonWidget, *mocks.MockWidget, ) { mockTitleBar := mocks.NewMockBoxWidget(t) mockFavicon := mocks.NewMockImageWidget(t) mockLabel := mocks.NewMockLabelWidget(t) mockButton := mocks.NewMockButtonWidget(t) mockContainer := mocks.NewMockWidget(t) // Title bar creation mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once() mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once() mockTitleBar.EXPECT().SetVexpand(false).Once() // Favicon mockFactory.EXPECT().NewImage().Return(mockFavicon).Once() mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once() mockFavicon.EXPECT().SetPixelSize(16).Once() mockTitleBar.EXPECT().Append(mockFavicon).Once() // Label mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once() mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once() mockLabel.EXPECT().SetMaxWidthChars(30).Once() mockLabel.EXPECT().SetHexpand(true).Once() mockLabel.EXPECT().SetXalign(float32(0.0)).Once() mockTitleBar.EXPECT().Append(mockLabel).Once() // Button wrapping title bar mockFactory.EXPECT().NewButton().Return(mockButton).Once() mockButton.EXPECT().SetChild(mockTitleBar).Once() mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once() mockButton.EXPECT().SetFocusOnClick(false).Once() mockButton.EXPECT().SetVexpand(false).Once() mockButton.EXPECT().SetHexpand(true).Once() mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once() // Adding to main box mockBox.EXPECT().Append(mockButton).Once() mockBox.EXPECT().Append(mockContainer).Once() return mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer } func TestNewStackedView_EmptyStack(t *testing.T) { // Arrange mockFactory, _ := setupMockFactory(t) // Act sv := layout.NewStackedView(mockFactory) // Assert require.NotNil(t, sv) assert.Equal(t, 0, sv.Count()) assert.Equal(t, -1, sv.ActiveIndex()) } func TestAddPane_SinglePane_BecomesActive(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon _ = mockLabel _ = mockButton // Visibility updates for active pane mockTitleBar.EXPECT().GetParent().Return(nil).Maybe() mockContainer.EXPECT().SetVisible(true).Once() mockTitleBar.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) // Act index := sv.AddPane(ctx, "pane-1", "Test Page", "web-browser-symbolic", mockContainer) // Assert assert.Equal(t, 0, index) assert.Equal(t, 1, sv.Count()) assert.Equal(t, 0, sv.ActiveIndex()) } func TestAddPane_MultiplePanes_LastBecomesActive(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) // First pane mockTitleBar1, mockFavicon1, mockLabel1, mockButton1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon1 _ = mockLabel1 _ = mockButton1 mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() mockContainer1.EXPECT().SetVisible(true).Once() mockTitleBar1.EXPECT().AddCssClass("active").Once() // Second pane - first pane becomes inactive mockTitleBar2, mockFavicon2, mockLabel2, mockButton2, mockContainer2 := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon2 _ = mockLabel2 _ = mockButton2 // When second pane is added, update visibility for both mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() mockContainer1.EXPECT().SetVisible(false).Once() mockTitleBar1.EXPECT().RemoveCssClass("active").Once() mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() mockContainer2.EXPECT().SetVisible(true).Once() mockTitleBar2.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) // Act sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1) index := sv.AddPane(ctx, "pane-2", "Page 2", "", mockContainer2) // Assert assert.Equal(t, 1, index) assert.Equal(t, 2, sv.Count()) assert.Equal(t, 1, sv.ActiveIndex()) } func TestRemovePane_MiddlePane(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) // Setup 3 panes (simplified - just track the key behaviors) containers := make([]*mocks.MockWidget, 3) titleBars := make([]*mocks.MockBoxWidget, 3) buttons := make([]*mocks.MockButtonWidget, 3) for i := 0; i < 3; i++ { var btn *mocks.MockButtonWidget titleBars[i], _, _, btn, containers[i] = setupPaneMocks(t, mockFactory, mockBox) buttons[i] = btn } // Allow any visibility calls during setup and removal for i := 0; i < 3; i++ { titleBars[i].EXPECT().GetParent().Return(buttons[i]).Maybe() containers[i].EXPECT().SetVisible(mock.Anything).Maybe() titleBars[i].EXPECT().AddCssClass("active").Maybe() titleBars[i].EXPECT().RemoveCssClass("active").Maybe() // Button visibility changes when panes are activated/deactivated buttons[i].EXPECT().SetVisible(mock.Anything).Maybe() } sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0]) sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1]) sv.AddPane(ctx, "pane-3", "Page 3", "", containers[2]) // Remove middle pane (index 1) // The parent of titleBar is the button widget mockBox.EXPECT().Remove(buttons[1]).Once() mockBox.EXPECT().Remove(containers[1]).Once() // Act err := sv.RemovePane(ctx, 1) // Assert require.NoError(t, err) assert.Equal(t, 2, sv.Count()) } func TestRemovePane_LastPane_ReturnsError(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon _ = mockLabel _ = mockButton mockTitleBar.EXPECT().GetParent().Return(nil).Maybe() mockContainer.EXPECT().SetVisible(true).Once() mockTitleBar.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Only Page", "", mockContainer) // Act err := sv.RemovePane(ctx, 0) // Assert require.ErrorIs(t, err, layout.ErrCannotRemoveLastPane) assert.Equal(t, 1, sv.Count()) } func TestRemovePane_EmptyStack_ReturnsError(t *testing.T) { // Arrange ctx := context.Background() mockFactory, _ := setupMockFactory(t) sv := layout.NewStackedView(mockFactory) // Act err := sv.RemovePane(ctx, 0) // Assert require.ErrorIs(t, err, layout.ErrStackEmpty) } func TestRemovePane_IndexOutOfBounds(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon _ = mockLabel _ = mockButton mockTitleBar.EXPECT().GetParent().Return(nil).Maybe() mockContainer.EXPECT().SetVisible(true).Once() mockTitleBar.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page", "", mockContainer) // Act err := sv.RemovePane(ctx, 5) // Assert require.ErrorIs(t, err, layout.ErrIndexOutOfBounds) } func TestSetActive_ValidIndex(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) containers := make([]*mocks.MockWidget, 2) titleBars := make([]*mocks.MockBoxWidget, 2) for i := 0; i < 2; i++ { titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon _ = mockOverlay _ = mockSpinner titleBars[i] = titleBar containers[i] = container titleBars[i].EXPECT().GetParent().Return(nil).Maybe() containers[i].EXPECT().SetVisible(mock.Anything).Maybe() titleBars[i].EXPECT().AddCssClass("active").Maybe() titleBars[i].EXPECT().RemoveCssClass("active").Maybe() } sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0]) sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1]) // Act - switch back to first pane err := sv.SetActive(ctx, 0) // Assert require.NoError(t, err) assert.Equal(t, 0, sv.ActiveIndex()) } func TestSetActive_OutOfBounds(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon _ = mockLabel _ = mockButton mockTitleBar.EXPECT().GetParent().Return(nil).Maybe() mockContainer.EXPECT().SetVisible(true).Once() mockTitleBar.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page", "", mockContainer) // Act err := sv.SetActive(ctx, 5) // Assert require.ErrorIs(t, err, layout.ErrIndexOutOfBounds) } func TestSetActive_EmptyStack(t *testing.T) { // Arrange ctx := context.Background() mockFactory, _ := setupMockFactory(t) sv := layout.NewStackedView(mockFactory) // Act err := sv.SetActive(ctx, 0) // Assert require.ErrorIs(t, err, layout.ErrStackEmpty) } func TestUpdateTitle(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) mockTitleBar, _, mockLabel, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox) mockTitleBar.EXPECT().GetParent().Return(nil).Maybe() mockContainer.EXPECT().SetVisible(true).Once() mockTitleBar.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Old Title", "", mockContainer) mockLabel.EXPECT().SetText("New Title").Once() // Act err := sv.UpdateTitle(0, "New Title") // Assert require.NoError(t, err) } func TestUpdateTitle_InvalidIndex(t *testing.T) { // Arrange mockFactory, _ := setupMockFactory(t) sv := layout.NewStackedView(mockFactory) // Act err := sv.UpdateTitle(0, "Title") // Assert require.ErrorIs(t, err, layout.ErrIndexOutOfBounds) } func TestUpdateFavicon(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) mockTitleBar, mockFavicon, _, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox) mockTitleBar.EXPECT().GetParent().Return(nil).Maybe() mockContainer.EXPECT().SetVisible(true).Once() mockTitleBar.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page", "", mockContainer) mockFavicon.EXPECT().SetFromIconName("new-icon").Once() // Act err := sv.UpdateFavicon(0, "new-icon") // Assert require.NoError(t, err) } func TestGetContainer(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) mockTitleBar, mockFavicon, mockOverlay, mockSpinner, mockContainer := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon _ = mockOverlay _ = mockSpinner mockTitleBar.EXPECT().GetParent().Return(nil).Maybe() mockContainer.EXPECT().SetVisible(true).Once() mockTitleBar.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page", "", mockContainer) // Act container, err := sv.GetContainer(0) // Assert require.NoError(t, err) assert.Equal(t, mockContainer, container) } func TestGetContainer_InvalidIndex(t *testing.T) { // Arrange mockFactory, _ := setupMockFactory(t) sv := layout.NewStackedView(mockFactory) // Act container, err := sv.GetContainer(0) // Assert require.ErrorIs(t, err, layout.ErrIndexOutOfBounds) assert.Nil(t, container) } func TestWidget_ReturnsBoxWidget(t *testing.T) { // Arrange mockFactory, mockBox := setupMockFactory(t) sv := layout.NewStackedView(mockFactory) // Act widget := sv.Widget() // Assert assert.Equal(t, mockBox, widget) } func TestNavigateNext_WrapsAround(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) containers := make([]*mocks.MockWidget, 2) titleBars := make([]*mocks.MockBoxWidget, 2) for i := 0; i < 2; i++ { titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon _ = mockOverlay _ = mockSpinner titleBars[i] = titleBar containers[i] = container titleBars[i].EXPECT().GetParent().Return(nil).Maybe() containers[i].EXPECT().SetVisible(mock.Anything).Maybe() titleBars[i].EXPECT().AddCssClass("active").Maybe() titleBars[i].EXPECT().RemoveCssClass("active").Maybe() } sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0]) sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1]) // Currently at index 1 (last added) assert.Equal(t, 1, sv.ActiveIndex()) // Act - navigate next should wrap to 0 err := sv.NavigateNext(ctx) // Assert require.NoError(t, err) assert.Equal(t, 0, sv.ActiveIndex()) } func TestNavigatePrevious_WrapsAround(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) containers := make([]*mocks.MockWidget, 2) titleBars := make([]*mocks.MockBoxWidget, 2) for i := 0; i < 2; i++ { titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon _ = mockOverlay _ = mockSpinner titleBars[i] = titleBar containers[i] = container titleBars[i].EXPECT().GetParent().Return(nil).Maybe() containers[i].EXPECT().SetVisible(mock.Anything).Maybe() titleBars[i].EXPECT().AddCssClass("active").Maybe() titleBars[i].EXPECT().RemoveCssClass("active").Maybe() } sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0]) sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1]) // Set to first pane sv.SetActive(ctx, 0) // Act - navigate previous should wrap to 1 err := sv.NavigatePrevious(ctx) // Assert require.NoError(t, err) assert.Equal(t, 1, sv.ActiveIndex()) } func TestNavigateNext_EmptyStack(t *testing.T) { // Arrange ctx := context.Background() mockFactory, _ := setupMockFactory(t) sv := layout.NewStackedView(mockFactory) // Act err := sv.NavigateNext(ctx) // Assert require.ErrorIs(t, err, layout.ErrStackEmpty) } func TestNavigatePrevious_EmptyStack(t *testing.T) { // Arrange ctx := context.Background() mockFactory, _ := setupMockFactory(t) sv := layout.NewStackedView(mockFactory) // Act err := sv.NavigatePrevious(ctx) // Assert require.ErrorIs(t, err, layout.ErrStackEmpty) } func TestSetOnActivate_Callback(t *testing.T) { // Arrange mockFactory, _ := setupMockFactory(t) sv := layout.NewStackedView(mockFactory) callback := func(index int) { _ = index // Used to test callback registration } // Act sv.SetOnActivate(callback) // We can't easily test the callback being triggered since it requires // simulating a button click. The test verifies the method doesn't panic. // Integration tests would verify the actual callback invocation. } // setupInsertPaneMocks creates mocks needed for InsertPaneAfter with position-aware insertion. // Returns only the titleBar and container mocks that are needed by test assertions. func setupInsertPaneMocks( t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox *mocks.MockBoxWidget, siblingContainer layout.Widget, ) (*mocks.MockBoxWidget, *mocks.MockWidget) { mockTitleBar := mocks.NewMockBoxWidget(t) mockFavicon := mocks.NewMockImageWidget(t) mockLabel := mocks.NewMockLabelWidget(t) mockButton := mocks.NewMockButtonWidget(t) mockContainer := mocks.NewMockWidget(t) // Title bar creation mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once() mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once() mockTitleBar.EXPECT().SetVexpand(false).Once() // Favicon mockFactory.EXPECT().NewImage().Return(mockFavicon).Once() mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once() mockFavicon.EXPECT().SetPixelSize(16).Once() mockTitleBar.EXPECT().Append(mockFavicon).Once() // Label mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once() mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once() mockLabel.EXPECT().SetMaxWidthChars(30).Once() mockLabel.EXPECT().SetHexpand(true).Once() mockLabel.EXPECT().SetXalign(float32(0.0)).Once() mockTitleBar.EXPECT().Append(mockLabel).Once() // Button wrapping title bar mockFactory.EXPECT().NewButton().Return(mockButton).Once() mockButton.EXPECT().SetChild(mockTitleBar).Once() mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once() mockButton.EXPECT().SetFocusOnClick(false).Once() mockButton.EXPECT().SetVexpand(false).Once() mockButton.EXPECT().SetHexpand(true).Once() mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once() // Position-aware insertion using InsertChildAfter if siblingContainer != nil { mockBox.EXPECT().InsertChildAfter(mockButton, siblingContainer).Once() mockBox.EXPECT().InsertChildAfter(mockContainer, mockButton).Once() } else { // Insert at beginning using Prepend mockBox.EXPECT().Prepend(mockContainer).Once() mockBox.EXPECT().Prepend(mockButton).Once() } return mockTitleBar, mockContainer } func TestInsertPaneAfter_AtBeginning(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) // First pane - appended normally mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon1 _ = mockOverlay1 _ = mockSpinner1 mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() mockContainer1.EXPECT().SetVisible(true).Once() mockTitleBar1.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1) // Second pane - inserted at beginning (afterIndex=-1) mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, nil) // Visibility updates mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() mockContainer1.EXPECT().SetVisible(false).Once() mockTitleBar1.EXPECT().RemoveCssClass("active").Once() mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() mockContainer2.EXPECT().SetVisible(true).Once() mockTitleBar2.EXPECT().AddCssClass("active").Once() // Act index := sv.InsertPaneAfter(ctx, -1, "pane-0", "Page 0", "", mockContainer2) // Assert assert.Equal(t, 0, index) assert.Equal(t, 2, sv.Count()) assert.Equal(t, 0, sv.ActiveIndex()) } func TestInsertPaneAfter_InMiddle(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) // Setup 2 initial panes containers := make([]*mocks.MockWidget, 2) titleBars := make([]*mocks.MockBoxWidget, 2) for i := 0; i < 2; i++ { titleBars[i], _, _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox) titleBars[i].EXPECT().GetParent().Return(nil).Maybe() containers[i].EXPECT().SetVisible(mock.Anything).Maybe() titleBars[i].EXPECT().AddCssClass("active").Maybe() titleBars[i].EXPECT().RemoveCssClass("active").Maybe() } sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page 1", "", containers[0]) sv.AddPane(ctx, "pane-2", "Page 2", "", containers[1]) // Set active to first pane require.NoError(t, sv.SetActive(ctx, 0)) // Insert new pane after index 0 (should become index 1) mockTitleBar3, mockContainer3 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[0]) mockTitleBar3.EXPECT().GetParent().Return(nil).Maybe() mockContainer3.EXPECT().SetVisible(mock.Anything).Maybe() mockTitleBar3.EXPECT().AddCssClass("active").Maybe() mockTitleBar3.EXPECT().RemoveCssClass("active").Maybe() // Act index := sv.InsertPaneAfter(ctx, 0, "pane-1.5", "Page 1.5", "", mockContainer3) // Assert assert.Equal(t, 1, index) assert.Equal(t, 3, sv.Count()) assert.Equal(t, 1, sv.ActiveIndex()) // New pane becomes active } func TestInsertPaneAfter_AtEnd(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) // First pane mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon1 _ = mockOverlay1 _ = mockSpinner1 mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() mockContainer1.EXPECT().SetVisible(true).Once() mockTitleBar1.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1) // Insert after last pane (afterIndex=0, becomes index 1) mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1) mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() mockContainer1.EXPECT().SetVisible(false).Once() mockTitleBar1.EXPECT().RemoveCssClass("active").Once() mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() mockContainer2.EXPECT().SetVisible(true).Once() mockTitleBar2.EXPECT().AddCssClass("active").Once() // Act index := sv.InsertPaneAfter(ctx, 0, "pane-2", "Page 2", "", mockContainer2) // Assert assert.Equal(t, 1, index) assert.Equal(t, 2, sv.Count()) assert.Equal(t, 1, sv.ActiveIndex()) } func TestInsertPaneAfter_MaintainsOrder(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) // Setup 3 initial panes: A, B, C containers := make([]*mocks.MockWidget, 3) titleBars := make([]*mocks.MockBoxWidget, 3) for i := 0; i < 3; i++ { titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon _ = mockOverlay _ = mockSpinner titleBars[i] = titleBar containers[i] = container titleBars[i].EXPECT().GetParent().Return(nil).Maybe() containers[i].EXPECT().SetVisible(mock.Anything).Maybe() titleBars[i].EXPECT().AddCssClass("active").Maybe() titleBars[i].EXPECT().RemoveCssClass("active").Maybe() } sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-a", "A", "", containers[0]) // index 0 sv.AddPane(ctx, "pane-b", "B", "", containers[1]) // index 1 sv.AddPane(ctx, "pane-c", "C", "", containers[2]) // index 2 // Navigate to pane B (index 1) require.NoError(t, sv.SetActive(ctx, 1)) // Insert D after B (should be at index 2, C moves to index 3) mockTitleBar4, mockContainer4 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[1]) mockTitleBar4.EXPECT().GetParent().Return(nil).Maybe() mockContainer4.EXPECT().SetVisible(mock.Anything).Maybe() mockTitleBar4.EXPECT().AddCssClass("active").Maybe() mockTitleBar4.EXPECT().RemoveCssClass("active").Maybe() // Act index := sv.InsertPaneAfter(ctx, 1, "pane-d", "D", "", mockContainer4) // Assert: Order should be A(0), B(1), D(2), C(3) assert.Equal(t, 2, index) assert.Equal(t, 4, sv.Count()) assert.Equal(t, 2, sv.ActiveIndex()) // D is now active // Verify we can still get containers at expected indices containerA, _ := sv.GetContainer(0) containerB, _ := sv.GetContainer(1) containerD, _ := sv.GetContainer(2) containerC, _ := sv.GetContainer(3) assert.Equal(t, containers[0], containerA) assert.Equal(t, containers[1], containerB) assert.Equal(t, mockContainer4, containerD) assert.Equal(t, containers[2], containerC) } func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) { // Arrange ctx := context.Background() mockFactory, mockBox := setupMockFactory(t) // First pane mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) _ = mockFavicon1 _ = mockOverlay1 _ = mockSpinner1 mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() mockContainer1.EXPECT().SetVisible(true).Once() mockTitleBar1.EXPECT().AddCssClass("active").Once() sv := layout.NewStackedView(mockFactory) sv.AddPane(ctx, "pane-1", "Page 1", "", mockContainer1) // Try to insert at invalid index (100) - should clamp to end mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1) mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() mockContainer1.EXPECT().SetVisible(false).Once() mockTitleBar1.EXPECT().RemoveCssClass("active").Once() mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() mockContainer2.EXPECT().SetVisible(true).Once() mockTitleBar2.EXPECT().AddCssClass("active").Once() // Act - afterIndex=100 should be clamped to 0 (last valid index) index := sv.InsertPaneAfter(ctx, 100, "pane-2", "Page 2", "", mockContainer2) // Assert assert.Equal(t, 1, index) assert.Equal(t, 2, sv.Count()) }