feat(layout): add position-aware InsertPaneAfter for stacked views
Implement InsertPaneAfter method to insert new panes immediately after the current active pane, rather than always appending to the end. Update StackedPaneManager to use this new method. Includes comprehensive tests for position-aware insertion behavior with various edge cases.
- commit
- 6f9d17f4fadd86cb0fe39cc7bc84685ffc3b4e57
- author
- brice · · 2025-12-17 22:25 UTC
- committer
- brice · · 2025-12-17 20:36 UTC
- parent
- 786d68a
- signature
- unsigned
3 files · +384 -3
M internal/ui/component/stacked_pane_manager.go +7 -3
| @@ -60,19 +60,23 @@ func (spm *StackedPaneManager) AddPaneToStack( | |||
| 60 | 60 | widget.Unparent() | |
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | - | // Add to the stack (this makes it active automatically) | |
| 63 | + | // Add to the stack after the current active pane (not at end) | |
| 64 | 64 | if title == "" { | |
| 65 | 65 | title = "Untitled" | |
| 66 | 66 | } | |
| 67 | 67 | ||
| 68 | + | // Get current active index to insert after it | |
| 69 | + | currentActiveIndex := stackedView.ActiveIndex() | |
| 70 | + | ||
| 68 | 71 | log.Debug(). | |
| 69 | 72 | Str("active_pane", string(activePaneID)). | |
| 70 | 73 | Str("new_pane", string(newPaneView.PaneID())). | |
| 71 | 74 | Str("title", title). | |
| 75 | + | Int("current_active_index", currentActiveIndex). | |
| 72 | 76 | Int("stack_size_before", stackedView.Count()). | |
| 73 | - | Msg("StackedPaneManager: adding pane to stack") | |
| 77 | + | Msg("StackedPaneManager: inserting pane after active position") | |
| 74 | 78 | ||
| 75 | - | stackedView.AddPane(ctx, title, "", widget) | |
| 79 | + | stackedView.InsertPaneAfter(ctx, currentActiveIndex, title, "", widget) | |
| 76 | 80 | ||
| 77 | 81 | // Register the new pane in the TreeRenderer's tracking | |
| 78 | 82 | tr.RegisterPaneInStack(string(newPaneView.PaneID()), stackedView) | |
| @@ -60,19 +60,23 @@ func (spm *StackedPaneManager) AddPaneToStack( | |||||
| 60 | widget.Unparent() | 60 | widget.Unparent() | ||
| 61 | } | 61 | } | ||
| 62 | 62 | ||||
| 63 | - | // Add to the stack (this makes it active automatically) | 63 | + | // Add to the stack after the current active pane (not at end) |
| 64 | if title == "" { | 64 | if title == "" { | ||
| 65 | title = "Untitled" | 65 | title = "Untitled" | ||
| 66 | } | 66 | } | ||
| 67 | 67 | ||||
| 68 | + | // Get current active index to insert after it | |||
| 69 | + | currentActiveIndex := stackedView.ActiveIndex() | |||
| 70 | + | ||||
| 68 | log.Debug(). | 71 | log.Debug(). | ||
| 69 | Str("active_pane", string(activePaneID)). | 72 | Str("active_pane", string(activePaneID)). | ||
| 70 | Str("new_pane", string(newPaneView.PaneID())). | 73 | Str("new_pane", string(newPaneView.PaneID())). | ||
| 71 | Str("title", title). | 74 | Str("title", title). | ||
| 75 | + | Int("current_active_index", currentActiveIndex). | |||
| 72 | Int("stack_size_before", stackedView.Count()). | 76 | Int("stack_size_before", stackedView.Count()). | ||
| 73 | - | Msg("StackedPaneManager: adding pane to stack") | 77 | + | Msg("StackedPaneManager: inserting pane after active position") |
| 74 | 78 | ||||
| 75 | - | stackedView.AddPane(ctx, title, "", widget) | 79 | + | stackedView.InsertPaneAfter(ctx, currentActiveIndex, title, "", widget) |
| 76 | 80 | ||||
| 77 | // Register the new pane in the TreeRenderer's tracking | 81 | // Register the new pane in the TreeRenderer's tracking | ||
| 78 | tr.RegisterPaneInStack(string(newPaneView.PaneID()), stackedView) | 82 | tr.RegisterPaneInStack(string(newPaneView.PaneID()), stackedView) | ||
M internal/ui/layout/stacked.go +128 -0
| @@ -141,6 +141,134 @@ func (sv *StackedView) AddPane(ctx context.Context, title, faviconIconName strin | |||
| 141 | 141 | return index | |
| 142 | 142 | } | |
| 143 | 143 | ||
| 144 | + | // InsertPaneAfter inserts a new pane after the specified index position. | |
| 145 | + | // Use afterIndex=-1 to insert at the beginning. | |
| 146 | + | // The new pane becomes active (visible). | |
| 147 | + | // Returns the index where the pane was inserted. | |
| 148 | + | func (sv *StackedView) InsertPaneAfter( | |
| 149 | + | ctx context.Context, afterIndex int, title, faviconIconName string, container Widget, | |
| 150 | + | ) int { | |
| 151 | + | log := logging.FromContext(ctx) | |
| 152 | + | sv.mu.Lock() | |
| 153 | + | defer sv.mu.Unlock() | |
| 154 | + | ||
| 155 | + | // Validate afterIndex - clamp to valid range | |
| 156 | + | if afterIndex < -1 { | |
| 157 | + | afterIndex = -1 | |
| 158 | + | } | |
| 159 | + | if afterIndex >= len(sv.panes) { | |
| 160 | + | afterIndex = len(sv.panes) - 1 | |
| 161 | + | } | |
| 162 | + | insertIndex := afterIndex + 1 | |
| 163 | + | ||
| 164 | + | log.Debug(). | |
| 165 | + | Str("title", title). | |
| 166 | + | Int("after_index", afterIndex). | |
| 167 | + | Int("insert_index", insertIndex). | |
| 168 | + | Int("current_count", len(sv.panes)). | |
| 169 | + | Msg("StackedView.InsertPaneAfter called") | |
| 170 | + | ||
| 171 | + | // Create title bar - must not expand vertically | |
| 172 | + | titleBar := sv.factory.NewBox(OrientationHorizontal, 4) | |
| 173 | + | titleBar.AddCssClass("stacked-pane-titlebar") | |
| 174 | + | titleBar.SetVexpand(false) | |
| 175 | + | ||
| 176 | + | // Create favicon image | |
| 177 | + | favicon := sv.factory.NewImage() | |
| 178 | + | if faviconIconName != "" { | |
| 179 | + | favicon.SetFromIconName(faviconIconName) | |
| 180 | + | } else { | |
| 181 | + | favicon.SetFromIconName("web-browser-symbolic") | |
| 182 | + | } | |
| 183 | + | favicon.SetPixelSize(16) | |
| 184 | + | titleBar.Append(favicon) | |
| 185 | + | ||
| 186 | + | // Create title label | |
| 187 | + | label := sv.factory.NewLabel(title) | |
| 188 | + | label.SetEllipsize(EllipsizeEnd) | |
| 189 | + | label.SetMaxWidthChars(30) | |
| 190 | + | label.SetHexpand(true) | |
| 191 | + | label.SetXalign(0.0) | |
| 192 | + | titleBar.Append(label) | |
| 193 | + | ||
| 194 | + | // Make title bar clickable - ensure it doesn't expand vertically | |
| 195 | + | titleButton := sv.factory.NewButton() | |
| 196 | + | titleButton.SetChild(titleBar) | |
| 197 | + | titleButton.AddCssClass("stacked-pane-title-button") | |
| 198 | + | titleButton.SetFocusOnClick(false) | |
| 199 | + | titleButton.SetVexpand(false) | |
| 200 | + | titleButton.SetHexpand(true) | |
| 201 | + | ||
| 202 | + | pane := &stackedPane{ | |
| 203 | + | titleBar: titleBar, | |
| 204 | + | container: container, | |
| 205 | + | title: title, | |
| 206 | + | favicon: favicon, | |
| 207 | + | label: label, | |
| 208 | + | isActive: false, | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // Insert into slice at correct position | |
| 212 | + | sv.panes = append(sv.panes, nil) | |
| 213 | + | copy(sv.panes[insertIndex+1:], sv.panes[insertIndex:]) | |
| 214 | + | sv.panes[insertIndex] = pane | |
| 215 | + | ||
| 216 | + | // Connect click handler - use captured index | |
| 217 | + | capturedIndex := insertIndex | |
| 218 | + | titleButton.ConnectClicked(func() { | |
| 219 | + | sv.mu.RLock() | |
| 220 | + | callback := sv.onActivate | |
| 221 | + | sv.mu.RUnlock() | |
| 222 | + | ||
| 223 | + | if callback != nil { | |
| 224 | + | callback(capturedIndex) | |
| 225 | + | } | |
| 226 | + | }) | |
| 227 | + | ||
| 228 | + | // Insert widgets at correct position in GTK box | |
| 229 | + | if insertIndex > 0 && sv.panes[insertIndex-1] != nil { | |
| 230 | + | // Insert after the previous pane's container | |
| 231 | + | prevPane := sv.panes[insertIndex-1] | |
| 232 | + | if prevPane.container != nil { | |
| 233 | + | sv.box.InsertChildAfter(titleButton, prevPane.container) | |
| 234 | + | if container != nil { | |
| 235 | + | sv.box.InsertChildAfter(container, titleButton) | |
| 236 | + | } | |
| 237 | + | } else { | |
| 238 | + | // No container, insert after title bar's parent (the button) | |
| 239 | + | prevTitleParent := prevPane.titleBar.GetParent() | |
| 240 | + | if prevTitleParent != nil { | |
| 241 | + | sv.box.InsertChildAfter(titleButton, prevTitleParent) | |
| 242 | + | if container != nil { | |
| 243 | + | sv.box.InsertChildAfter(container, titleButton) | |
| 244 | + | } | |
| 245 | + | } else { | |
| 246 | + | // Fallback to append | |
| 247 | + | sv.box.Append(titleButton) | |
| 248 | + | if container != nil { | |
| 249 | + | sv.box.Append(container) | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | } else { | |
| 254 | + | // Insert at beginning | |
| 255 | + | if container != nil { | |
| 256 | + | sv.box.Prepend(container) | |
| 257 | + | } | |
| 258 | + | sv.box.Prepend(titleButton) | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | // Set this pane as active | |
| 262 | + | sv.setActiveInternal(ctx, insertIndex) | |
| 263 | + | ||
| 264 | + | log.Debug(). | |
| 265 | + | Int("insert_index", insertIndex). | |
| 266 | + | Int("new_count", len(sv.panes)). | |
| 267 | + | Msg("StackedView.InsertPaneAfter completed") | |
| 268 | + | ||
| 269 | + | return insertIndex | |
| 270 | + | } | |
| 271 | + | ||
| 144 | 272 | // RemovePane removes a pane from the stack by index. | |
| 145 | 273 | // Returns an error if trying to remove the last pane. | |
| 146 | 274 | func (sv *StackedView) RemovePane(ctx context.Context, index int) error { | |
| @@ -141,6 +141,134 @@ func (sv *StackedView) AddPane(ctx context.Context, title, faviconIconName strin | |||||
| 141 | return index | 141 | return index | ||
| 142 | } | 142 | } | ||
| 143 | 143 | ||||
| 144 | + | // InsertPaneAfter inserts a new pane after the specified index position. | |||
| 145 | + | // Use afterIndex=-1 to insert at the beginning. | |||
| 146 | + | // The new pane becomes active (visible). | |||
| 147 | + | // Returns the index where the pane was inserted. | |||
| 148 | + | func (sv *StackedView) InsertPaneAfter( | |||
| 149 | + | ctx context.Context, afterIndex int, title, faviconIconName string, container Widget, | |||
| 150 | + | ) int { | |||
| 151 | + | log := logging.FromContext(ctx) | |||
| 152 | + | sv.mu.Lock() | |||
| 153 | + | defer sv.mu.Unlock() | |||
| 154 | + | ||||
| 155 | + | // Validate afterIndex - clamp to valid range | |||
| 156 | + | if afterIndex < -1 { | |||
| 157 | + | afterIndex = -1 | |||
| 158 | + | } | |||
| 159 | + | if afterIndex >= len(sv.panes) { | |||
| 160 | + | afterIndex = len(sv.panes) - 1 | |||
| 161 | + | } | |||
| 162 | + | insertIndex := afterIndex + 1 | |||
| 163 | + | ||||
| 164 | + | log.Debug(). | |||
| 165 | + | Str("title", title). | |||
| 166 | + | Int("after_index", afterIndex). | |||
| 167 | + | Int("insert_index", insertIndex). | |||
| 168 | + | Int("current_count", len(sv.panes)). | |||
| 169 | + | Msg("StackedView.InsertPaneAfter called") | |||
| 170 | + | ||||
| 171 | + | // Create title bar - must not expand vertically | |||
| 172 | + | titleBar := sv.factory.NewBox(OrientationHorizontal, 4) | |||
| 173 | + | titleBar.AddCssClass("stacked-pane-titlebar") | |||
| 174 | + | titleBar.SetVexpand(false) | |||
| 175 | + | ||||
| 176 | + | // Create favicon image | |||
| 177 | + | favicon := sv.factory.NewImage() | |||
| 178 | + | if faviconIconName != "" { | |||
| 179 | + | favicon.SetFromIconName(faviconIconName) | |||
| 180 | + | } else { | |||
| 181 | + | favicon.SetFromIconName("web-browser-symbolic") | |||
| 182 | + | } | |||
| 183 | + | favicon.SetPixelSize(16) | |||
| 184 | + | titleBar.Append(favicon) | |||
| 185 | + | ||||
| 186 | + | // Create title label | |||
| 187 | + | label := sv.factory.NewLabel(title) | |||
| 188 | + | label.SetEllipsize(EllipsizeEnd) | |||
| 189 | + | label.SetMaxWidthChars(30) | |||
| 190 | + | label.SetHexpand(true) | |||
| 191 | + | label.SetXalign(0.0) | |||
| 192 | + | titleBar.Append(label) | |||
| 193 | + | ||||
| 194 | + | // Make title bar clickable - ensure it doesn't expand vertically | |||
| 195 | + | titleButton := sv.factory.NewButton() | |||
| 196 | + | titleButton.SetChild(titleBar) | |||
| 197 | + | titleButton.AddCssClass("stacked-pane-title-button") | |||
| 198 | + | titleButton.SetFocusOnClick(false) | |||
| 199 | + | titleButton.SetVexpand(false) | |||
| 200 | + | titleButton.SetHexpand(true) | |||
| 201 | + | ||||
| 202 | + | pane := &stackedPane{ | |||
| 203 | + | titleBar: titleBar, | |||
| 204 | + | container: container, | |||
| 205 | + | title: title, | |||
| 206 | + | favicon: favicon, | |||
| 207 | + | label: label, | |||
| 208 | + | isActive: false, | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // Insert into slice at correct position | |||
| 212 | + | sv.panes = append(sv.panes, nil) | |||
| 213 | + | copy(sv.panes[insertIndex+1:], sv.panes[insertIndex:]) | |||
| 214 | + | sv.panes[insertIndex] = pane | |||
| 215 | + | ||||
| 216 | + | // Connect click handler - use captured index | |||
| 217 | + | capturedIndex := insertIndex | |||
| 218 | + | titleButton.ConnectClicked(func() { | |||
| 219 | + | sv.mu.RLock() | |||
| 220 | + | callback := sv.onActivate | |||
| 221 | + | sv.mu.RUnlock() | |||
| 222 | + | ||||
| 223 | + | if callback != nil { | |||
| 224 | + | callback(capturedIndex) | |||
| 225 | + | } | |||
| 226 | + | }) | |||
| 227 | + | ||||
| 228 | + | // Insert widgets at correct position in GTK box | |||
| 229 | + | if insertIndex > 0 && sv.panes[insertIndex-1] != nil { | |||
| 230 | + | // Insert after the previous pane's container | |||
| 231 | + | prevPane := sv.panes[insertIndex-1] | |||
| 232 | + | if prevPane.container != nil { | |||
| 233 | + | sv.box.InsertChildAfter(titleButton, prevPane.container) | |||
| 234 | + | if container != nil { | |||
| 235 | + | sv.box.InsertChildAfter(container, titleButton) | |||
| 236 | + | } | |||
| 237 | + | } else { | |||
| 238 | + | // No container, insert after title bar's parent (the button) | |||
| 239 | + | prevTitleParent := prevPane.titleBar.GetParent() | |||
| 240 | + | if prevTitleParent != nil { | |||
| 241 | + | sv.box.InsertChildAfter(titleButton, prevTitleParent) | |||
| 242 | + | if container != nil { | |||
| 243 | + | sv.box.InsertChildAfter(container, titleButton) | |||
| 244 | + | } | |||
| 245 | + | } else { | |||
| 246 | + | // Fallback to append | |||
| 247 | + | sv.box.Append(titleButton) | |||
| 248 | + | if container != nil { | |||
| 249 | + | sv.box.Append(container) | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | } else { | |||
| 254 | + | // Insert at beginning | |||
| 255 | + | if container != nil { | |||
| 256 | + | sv.box.Prepend(container) | |||
| 257 | + | } | |||
| 258 | + | sv.box.Prepend(titleButton) | |||
| 259 | + | } | |||
| 260 | + | ||||
| 261 | + | // Set this pane as active | |||
| 262 | + | sv.setActiveInternal(ctx, insertIndex) | |||
| 263 | + | ||||
| 264 | + | log.Debug(). | |||
| 265 | + | Int("insert_index", insertIndex). | |||
| 266 | + | Int("new_count", len(sv.panes)). | |||
| 267 | + | Msg("StackedView.InsertPaneAfter completed") | |||
| 268 | + | ||||
| 269 | + | return insertIndex | |||
| 270 | + | } | |||
| 271 | + | ||||
| 144 | // RemovePane removes a pane from the stack by index. | 272 | // RemovePane removes a pane from the stack by index. | ||
| 145 | // Returns an error if trying to remove the last pane. | 273 | // Returns an error if trying to remove the last pane. | ||
| 146 | func (sv *StackedView) RemovePane(ctx context.Context, index int) error { | 274 | func (sv *StackedView) RemovePane(ctx context.Context, index int) error { | ||
M internal/ui/layout/stacked_test.go +249 -0
| @@ -504,3 +504,252 @@ func TestSetOnActivate_Callback(t *testing.T) { | |||
| 504 | 504 | // simulating a button click. The test verifies the method doesn't panic. | |
| 505 | 505 | // Integration tests would verify the actual callback invocation. | |
| 506 | 506 | } | |
| 507 | + | ||
| 508 | + | // setupInsertPaneMocks creates mocks needed for InsertPaneAfter with position-aware insertion. | |
| 509 | + | // Returns only the titleBar and container mocks that are needed by test assertions. | |
| 510 | + | func setupInsertPaneMocks( | |
| 511 | + | t *testing.T, | |
| 512 | + | mockFactory *mocks.MockWidgetFactory, | |
| 513 | + | mockBox *mocks.MockBoxWidget, | |
| 514 | + | siblingContainer layout.Widget, | |
| 515 | + | ) (*mocks.MockBoxWidget, *mocks.MockWidget) { | |
| 516 | + | mockTitleBar := mocks.NewMockBoxWidget(t) | |
| 517 | + | mockFavicon := mocks.NewMockImageWidget(t) | |
| 518 | + | mockLabel := mocks.NewMockLabelWidget(t) | |
| 519 | + | mockButton := mocks.NewMockButtonWidget(t) | |
| 520 | + | mockContainer := mocks.NewMockWidget(t) | |
| 521 | + | ||
| 522 | + | // Title bar creation | |
| 523 | + | mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once() | |
| 524 | + | mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once() | |
| 525 | + | mockTitleBar.EXPECT().SetVexpand(false).Once() | |
| 526 | + | ||
| 527 | + | // Favicon | |
| 528 | + | mockFactory.EXPECT().NewImage().Return(mockFavicon).Once() | |
| 529 | + | mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once() | |
| 530 | + | mockFavicon.EXPECT().SetPixelSize(16).Once() | |
| 531 | + | mockTitleBar.EXPECT().Append(mockFavicon).Once() | |
| 532 | + | ||
| 533 | + | // Label | |
| 534 | + | mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once() | |
| 535 | + | mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once() | |
| 536 | + | mockLabel.EXPECT().SetMaxWidthChars(30).Once() | |
| 537 | + | mockLabel.EXPECT().SetHexpand(true).Once() | |
| 538 | + | mockLabel.EXPECT().SetXalign(float32(0.0)).Once() | |
| 539 | + | mockTitleBar.EXPECT().Append(mockLabel).Once() | |
| 540 | + | ||
| 541 | + | // Button wrapping title bar | |
| 542 | + | mockFactory.EXPECT().NewButton().Return(mockButton).Once() | |
| 543 | + | mockButton.EXPECT().SetChild(mockTitleBar).Once() | |
| 544 | + | mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once() | |
| 545 | + | mockButton.EXPECT().SetFocusOnClick(false).Once() | |
| 546 | + | mockButton.EXPECT().SetVexpand(false).Once() | |
| 547 | + | mockButton.EXPECT().SetHexpand(true).Once() | |
| 548 | + | mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once() | |
| 549 | + | ||
| 550 | + | // Position-aware insertion using InsertChildAfter | |
| 551 | + | if siblingContainer != nil { | |
| 552 | + | mockBox.EXPECT().InsertChildAfter(mockButton, siblingContainer).Once() | |
| 553 | + | mockBox.EXPECT().InsertChildAfter(mockContainer, mockButton).Once() | |
| 554 | + | } else { | |
| 555 | + | // Insert at beginning using Prepend | |
| 556 | + | mockBox.EXPECT().Prepend(mockContainer).Once() | |
| 557 | + | mockBox.EXPECT().Prepend(mockButton).Once() | |
| 558 | + | } | |
| 559 | + | ||
| 560 | + | return mockTitleBar, mockContainer | |
| 561 | + | } | |
| 562 | + | ||
| 563 | + | func TestInsertPaneAfter_AtBeginning(t *testing.T) { | |
| 564 | + | // Arrange | |
| 565 | + | ctx := context.Background() | |
| 566 | + | mockFactory, mockBox := setupMockFactory(t) | |
| 567 | + | ||
| 568 | + | // First pane - appended normally | |
| 569 | + | mockTitleBar1, _, _, _, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 570 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |
| 571 | + | mockContainer1.EXPECT().SetVisible(true).Once() | |
| 572 | + | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |
| 573 | + | ||
| 574 | + | sv := layout.NewStackedView(mockFactory) | |
| 575 | + | sv.AddPane(ctx, "Page 1", "", mockContainer1) | |
| 576 | + | ||
| 577 | + | // Second pane - inserted at beginning (afterIndex=-1) | |
| 578 | + | mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, nil) | |
| 579 | + | ||
| 580 | + | // Visibility updates | |
| 581 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |
| 582 | + | mockContainer1.EXPECT().SetVisible(false).Once() | |
| 583 | + | mockTitleBar1.EXPECT().RemoveCssClass("active").Once() | |
| 584 | + | ||
| 585 | + | mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() | |
| 586 | + | mockContainer2.EXPECT().SetVisible(true).Once() | |
| 587 | + | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |
| 588 | + | ||
| 589 | + | // Act | |
| 590 | + | index := sv.InsertPaneAfter(ctx, -1, "Page 0", "", mockContainer2) | |
| 591 | + | ||
| 592 | + | // Assert | |
| 593 | + | assert.Equal(t, 0, index) | |
| 594 | + | assert.Equal(t, 2, sv.Count()) | |
| 595 | + | assert.Equal(t, 0, sv.ActiveIndex()) | |
| 596 | + | } | |
| 597 | + | ||
| 598 | + | func TestInsertPaneAfter_InMiddle(t *testing.T) { | |
| 599 | + | // Arrange | |
| 600 | + | ctx := context.Background() | |
| 601 | + | mockFactory, mockBox := setupMockFactory(t) | |
| 602 | + | ||
| 603 | + | // Setup 2 initial panes | |
| 604 | + | containers := make([]*mocks.MockWidget, 2) | |
| 605 | + | titleBars := make([]*mocks.MockBoxWidget, 2) | |
| 606 | + | ||
| 607 | + | for i := 0; i < 2; i++ { | |
| 608 | + | titleBars[i], _, _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox) | |
| 609 | + | titleBars[i].EXPECT().GetParent().Return(nil).Maybe() | |
| 610 | + | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 611 | + | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |
| 612 | + | titleBars[i].EXPECT().RemoveCssClass("active").Maybe() | |
| 613 | + | } | |
| 614 | + | ||
| 615 | + | sv := layout.NewStackedView(mockFactory) | |
| 616 | + | sv.AddPane(ctx, "Page 1", "", containers[0]) | |
| 617 | + | sv.AddPane(ctx, "Page 2", "", containers[1]) | |
| 618 | + | ||
| 619 | + | // Set active to first pane | |
| 620 | + | require.NoError(t, sv.SetActive(ctx, 0)) | |
| 621 | + | ||
| 622 | + | // Insert new pane after index 0 (should become index 1) | |
| 623 | + | mockTitleBar3, mockContainer3 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[0]) | |
| 624 | + | mockTitleBar3.EXPECT().GetParent().Return(nil).Maybe() | |
| 625 | + | mockContainer3.EXPECT().SetVisible(mock.Anything).Maybe() | |
| 626 | + | mockTitleBar3.EXPECT().AddCssClass("active").Maybe() | |
| 627 | + | mockTitleBar3.EXPECT().RemoveCssClass("active").Maybe() | |
| 628 | + | ||
| 629 | + | // Act | |
| 630 | + | index := sv.InsertPaneAfter(ctx, 0, "Page 1.5", "", mockContainer3) | |
| 631 | + | ||
| 632 | + | // Assert | |
| 633 | + | assert.Equal(t, 1, index) | |
| 634 | + | assert.Equal(t, 3, sv.Count()) | |
| 635 | + | assert.Equal(t, 1, sv.ActiveIndex()) // New pane becomes active | |
| 636 | + | } | |
| 637 | + | ||
| 638 | + | func TestInsertPaneAfter_AtEnd(t *testing.T) { | |
| 639 | + | // Arrange | |
| 640 | + | ctx := context.Background() | |
| 641 | + | mockFactory, mockBox := setupMockFactory(t) | |
| 642 | + | ||
| 643 | + | // First pane | |
| 644 | + | mockTitleBar1, _, _, _, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 645 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |
| 646 | + | mockContainer1.EXPECT().SetVisible(true).Once() | |
| 647 | + | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |
| 648 | + | ||
| 649 | + | sv := layout.NewStackedView(mockFactory) | |
| 650 | + | sv.AddPane(ctx, "Page 1", "", mockContainer1) | |
| 651 | + | ||
| 652 | + | // Insert after last pane (afterIndex=0, becomes index 1) | |
| 653 | + | mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1) | |
| 654 | + | ||
| 655 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |
| 656 | + | mockContainer1.EXPECT().SetVisible(false).Once() | |
| 657 | + | mockTitleBar1.EXPECT().RemoveCssClass("active").Once() | |
| 658 | + | ||
| 659 | + | mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() | |
| 660 | + | mockContainer2.EXPECT().SetVisible(true).Once() | |
| 661 | + | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |
| 662 | + | ||
| 663 | + | // Act | |
| 664 | + | index := sv.InsertPaneAfter(ctx, 0, "Page 2", "", mockContainer2) | |
| 665 | + | ||
| 666 | + | // Assert | |
| 667 | + | assert.Equal(t, 1, index) | |
| 668 | + | assert.Equal(t, 2, sv.Count()) | |
| 669 | + | assert.Equal(t, 1, sv.ActiveIndex()) | |
| 670 | + | } | |
| 671 | + | ||
| 672 | + | func TestInsertPaneAfter_MaintainsOrder(t *testing.T) { | |
| 673 | + | // Arrange | |
| 674 | + | ctx := context.Background() | |
| 675 | + | mockFactory, mockBox := setupMockFactory(t) | |
| 676 | + | ||
| 677 | + | // Setup 3 initial panes: A, B, C | |
| 678 | + | containers := make([]*mocks.MockWidget, 3) | |
| 679 | + | titleBars := make([]*mocks.MockBoxWidget, 3) | |
| 680 | + | ||
| 681 | + | for i := 0; i < 3; i++ { | |
| 682 | + | titleBars[i], _, _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox) | |
| 683 | + | titleBars[i].EXPECT().GetParent().Return(nil).Maybe() | |
| 684 | + | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 685 | + | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |
| 686 | + | titleBars[i].EXPECT().RemoveCssClass("active").Maybe() | |
| 687 | + | } | |
| 688 | + | ||
| 689 | + | sv := layout.NewStackedView(mockFactory) | |
| 690 | + | sv.AddPane(ctx, "A", "", containers[0]) // index 0 | |
| 691 | + | sv.AddPane(ctx, "B", "", containers[1]) // index 1 | |
| 692 | + | sv.AddPane(ctx, "C", "", containers[2]) // index 2 | |
| 693 | + | ||
| 694 | + | // Navigate to pane B (index 1) | |
| 695 | + | require.NoError(t, sv.SetActive(ctx, 1)) | |
| 696 | + | ||
| 697 | + | // Insert D after B (should be at index 2, C moves to index 3) | |
| 698 | + | mockTitleBar4, mockContainer4 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[1]) | |
| 699 | + | mockTitleBar4.EXPECT().GetParent().Return(nil).Maybe() | |
| 700 | + | mockContainer4.EXPECT().SetVisible(mock.Anything).Maybe() | |
| 701 | + | mockTitleBar4.EXPECT().AddCssClass("active").Maybe() | |
| 702 | + | mockTitleBar4.EXPECT().RemoveCssClass("active").Maybe() | |
| 703 | + | ||
| 704 | + | // Act | |
| 705 | + | index := sv.InsertPaneAfter(ctx, 1, "D", "", mockContainer4) | |
| 706 | + | ||
| 707 | + | // Assert: Order should be A(0), B(1), D(2), C(3) | |
| 708 | + | assert.Equal(t, 2, index) | |
| 709 | + | assert.Equal(t, 4, sv.Count()) | |
| 710 | + | assert.Equal(t, 2, sv.ActiveIndex()) // D is now active | |
| 711 | + | ||
| 712 | + | // Verify we can still get containers at expected indices | |
| 713 | + | containerA, _ := sv.GetContainer(0) | |
| 714 | + | containerB, _ := sv.GetContainer(1) | |
| 715 | + | containerD, _ := sv.GetContainer(2) | |
| 716 | + | containerC, _ := sv.GetContainer(3) | |
| 717 | + | ||
| 718 | + | assert.Equal(t, containers[0], containerA) | |
| 719 | + | assert.Equal(t, containers[1], containerB) | |
| 720 | + | assert.Equal(t, mockContainer4, containerD) | |
| 721 | + | assert.Equal(t, containers[2], containerC) | |
| 722 | + | } | |
| 723 | + | ||
| 724 | + | func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) { | |
| 725 | + | // Arrange | |
| 726 | + | ctx := context.Background() | |
| 727 | + | mockFactory, mockBox := setupMockFactory(t) | |
| 728 | + | ||
| 729 | + | // First pane | |
| 730 | + | mockTitleBar1, _, _, _, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 731 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |
| 732 | + | mockContainer1.EXPECT().SetVisible(true).Once() | |
| 733 | + | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |
| 734 | + | ||
| 735 | + | sv := layout.NewStackedView(mockFactory) | |
| 736 | + | sv.AddPane(ctx, "Page 1", "", mockContainer1) | |
| 737 | + | ||
| 738 | + | // Try to insert at invalid index (100) - should clamp to end | |
| 739 | + | mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1) | |
| 740 | + | ||
| 741 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |
| 742 | + | mockContainer1.EXPECT().SetVisible(false).Once() | |
| 743 | + | mockTitleBar1.EXPECT().RemoveCssClass("active").Once() | |
| 744 | + | ||
| 745 | + | mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() | |
| 746 | + | mockContainer2.EXPECT().SetVisible(true).Once() | |
| 747 | + | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |
| 748 | + | ||
| 749 | + | // Act - afterIndex=100 should be clamped to 0 (last valid index) | |
| 750 | + | index := sv.InsertPaneAfter(ctx, 100, "Page 2", "", mockContainer2) | |
| 751 | + | ||
| 752 | + | // Assert | |
| 753 | + | assert.Equal(t, 1, index) | |
| 754 | + | assert.Equal(t, 2, sv.Count()) | |
| 755 | + | } | |
| @@ -504,3 +504,252 @@ func TestSetOnActivate_Callback(t *testing.T) { | |||||
| 504 | // simulating a button click. The test verifies the method doesn't panic. | 504 | // simulating a button click. The test verifies the method doesn't panic. | ||
| 505 | // Integration tests would verify the actual callback invocation. | 505 | // Integration tests would verify the actual callback invocation. | ||
| 506 | } | 506 | } | ||
| 507 | + | ||||
| 508 | + | // setupInsertPaneMocks creates mocks needed for InsertPaneAfter with position-aware insertion. | |||
| 509 | + | // Returns only the titleBar and container mocks that are needed by test assertions. | |||
| 510 | + | func setupInsertPaneMocks( | |||
| 511 | + | t *testing.T, | |||
| 512 | + | mockFactory *mocks.MockWidgetFactory, | |||
| 513 | + | mockBox *mocks.MockBoxWidget, | |||
| 514 | + | siblingContainer layout.Widget, | |||
| 515 | + | ) (*mocks.MockBoxWidget, *mocks.MockWidget) { | |||
| 516 | + | mockTitleBar := mocks.NewMockBoxWidget(t) | |||
| 517 | + | mockFavicon := mocks.NewMockImageWidget(t) | |||
| 518 | + | mockLabel := mocks.NewMockLabelWidget(t) | |||
| 519 | + | mockButton := mocks.NewMockButtonWidget(t) | |||
| 520 | + | mockContainer := mocks.NewMockWidget(t) | |||
| 521 | + | ||||
| 522 | + | // Title bar creation | |||
| 523 | + | mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once() | |||
| 524 | + | mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once() | |||
| 525 | + | mockTitleBar.EXPECT().SetVexpand(false).Once() | |||
| 526 | + | ||||
| 527 | + | // Favicon | |||
| 528 | + | mockFactory.EXPECT().NewImage().Return(mockFavicon).Once() | |||
| 529 | + | mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once() | |||
| 530 | + | mockFavicon.EXPECT().SetPixelSize(16).Once() | |||
| 531 | + | mockTitleBar.EXPECT().Append(mockFavicon).Once() | |||
| 532 | + | ||||
| 533 | + | // Label | |||
| 534 | + | mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once() | |||
| 535 | + | mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once() | |||
| 536 | + | mockLabel.EXPECT().SetMaxWidthChars(30).Once() | |||
| 537 | + | mockLabel.EXPECT().SetHexpand(true).Once() | |||
| 538 | + | mockLabel.EXPECT().SetXalign(float32(0.0)).Once() | |||
| 539 | + | mockTitleBar.EXPECT().Append(mockLabel).Once() | |||
| 540 | + | ||||
| 541 | + | // Button wrapping title bar | |||
| 542 | + | mockFactory.EXPECT().NewButton().Return(mockButton).Once() | |||
| 543 | + | mockButton.EXPECT().SetChild(mockTitleBar).Once() | |||
| 544 | + | mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once() | |||
| 545 | + | mockButton.EXPECT().SetFocusOnClick(false).Once() | |||
| 546 | + | mockButton.EXPECT().SetVexpand(false).Once() | |||
| 547 | + | mockButton.EXPECT().SetHexpand(true).Once() | |||
| 548 | + | mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once() | |||
| 549 | + | ||||
| 550 | + | // Position-aware insertion using InsertChildAfter | |||
| 551 | + | if siblingContainer != nil { | |||
| 552 | + | mockBox.EXPECT().InsertChildAfter(mockButton, siblingContainer).Once() | |||
| 553 | + | mockBox.EXPECT().InsertChildAfter(mockContainer, mockButton).Once() | |||
| 554 | + | } else { | |||
| 555 | + | // Insert at beginning using Prepend | |||
| 556 | + | mockBox.EXPECT().Prepend(mockContainer).Once() | |||
| 557 | + | mockBox.EXPECT().Prepend(mockButton).Once() | |||
| 558 | + | } | |||
| 559 | + | ||||
| 560 | + | return mockTitleBar, mockContainer | |||
| 561 | + | } | |||
| 562 | + | ||||
| 563 | + | func TestInsertPaneAfter_AtBeginning(t *testing.T) { | |||
| 564 | + | // Arrange | |||
| 565 | + | ctx := context.Background() | |||
| 566 | + | mockFactory, mockBox := setupMockFactory(t) | |||
| 567 | + | ||||
| 568 | + | // First pane - appended normally | |||
| 569 | + | mockTitleBar1, _, _, _, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |||
| 570 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |||
| 571 | + | mockContainer1.EXPECT().SetVisible(true).Once() | |||
| 572 | + | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |||
| 573 | + | ||||
| 574 | + | sv := layout.NewStackedView(mockFactory) | |||
| 575 | + | sv.AddPane(ctx, "Page 1", "", mockContainer1) | |||
| 576 | + | ||||
| 577 | + | // Second pane - inserted at beginning (afterIndex=-1) | |||
| 578 | + | mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, nil) | |||
| 579 | + | ||||
| 580 | + | // Visibility updates | |||
| 581 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |||
| 582 | + | mockContainer1.EXPECT().SetVisible(false).Once() | |||
| 583 | + | mockTitleBar1.EXPECT().RemoveCssClass("active").Once() | |||
| 584 | + | ||||
| 585 | + | mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() | |||
| 586 | + | mockContainer2.EXPECT().SetVisible(true).Once() | |||
| 587 | + | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |||
| 588 | + | ||||
| 589 | + | // Act | |||
| 590 | + | index := sv.InsertPaneAfter(ctx, -1, "Page 0", "", mockContainer2) | |||
| 591 | + | ||||
| 592 | + | // Assert | |||
| 593 | + | assert.Equal(t, 0, index) | |||
| 594 | + | assert.Equal(t, 2, sv.Count()) | |||
| 595 | + | assert.Equal(t, 0, sv.ActiveIndex()) | |||
| 596 | + | } | |||
| 597 | + | ||||
| 598 | + | func TestInsertPaneAfter_InMiddle(t *testing.T) { | |||
| 599 | + | // Arrange | |||
| 600 | + | ctx := context.Background() | |||
| 601 | + | mockFactory, mockBox := setupMockFactory(t) | |||
| 602 | + | ||||
| 603 | + | // Setup 2 initial panes | |||
| 604 | + | containers := make([]*mocks.MockWidget, 2) | |||
| 605 | + | titleBars := make([]*mocks.MockBoxWidget, 2) | |||
| 606 | + | ||||
| 607 | + | for i := 0; i < 2; i++ { | |||
| 608 | + | titleBars[i], _, _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox) | |||
| 609 | + | titleBars[i].EXPECT().GetParent().Return(nil).Maybe() | |||
| 610 | + | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |||
| 611 | + | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |||
| 612 | + | titleBars[i].EXPECT().RemoveCssClass("active").Maybe() | |||
| 613 | + | } | |||
| 614 | + | ||||
| 615 | + | sv := layout.NewStackedView(mockFactory) | |||
| 616 | + | sv.AddPane(ctx, "Page 1", "", containers[0]) | |||
| 617 | + | sv.AddPane(ctx, "Page 2", "", containers[1]) | |||
| 618 | + | ||||
| 619 | + | // Set active to first pane | |||
| 620 | + | require.NoError(t, sv.SetActive(ctx, 0)) | |||
| 621 | + | ||||
| 622 | + | // Insert new pane after index 0 (should become index 1) | |||
| 623 | + | mockTitleBar3, mockContainer3 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[0]) | |||
| 624 | + | mockTitleBar3.EXPECT().GetParent().Return(nil).Maybe() | |||
| 625 | + | mockContainer3.EXPECT().SetVisible(mock.Anything).Maybe() | |||
| 626 | + | mockTitleBar3.EXPECT().AddCssClass("active").Maybe() | |||
| 627 | + | mockTitleBar3.EXPECT().RemoveCssClass("active").Maybe() | |||
| 628 | + | ||||
| 629 | + | // Act | |||
| 630 | + | index := sv.InsertPaneAfter(ctx, 0, "Page 1.5", "", mockContainer3) | |||
| 631 | + | ||||
| 632 | + | // Assert | |||
| 633 | + | assert.Equal(t, 1, index) | |||
| 634 | + | assert.Equal(t, 3, sv.Count()) | |||
| 635 | + | assert.Equal(t, 1, sv.ActiveIndex()) // New pane becomes active | |||
| 636 | + | } | |||
| 637 | + | ||||
| 638 | + | func TestInsertPaneAfter_AtEnd(t *testing.T) { | |||
| 639 | + | // Arrange | |||
| 640 | + | ctx := context.Background() | |||
| 641 | + | mockFactory, mockBox := setupMockFactory(t) | |||
| 642 | + | ||||
| 643 | + | // First pane | |||
| 644 | + | mockTitleBar1, _, _, _, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |||
| 645 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |||
| 646 | + | mockContainer1.EXPECT().SetVisible(true).Once() | |||
| 647 | + | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |||
| 648 | + | ||||
| 649 | + | sv := layout.NewStackedView(mockFactory) | |||
| 650 | + | sv.AddPane(ctx, "Page 1", "", mockContainer1) | |||
| 651 | + | ||||
| 652 | + | // Insert after last pane (afterIndex=0, becomes index 1) | |||
| 653 | + | mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1) | |||
| 654 | + | ||||
| 655 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |||
| 656 | + | mockContainer1.EXPECT().SetVisible(false).Once() | |||
| 657 | + | mockTitleBar1.EXPECT().RemoveCssClass("active").Once() | |||
| 658 | + | ||||
| 659 | + | mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() | |||
| 660 | + | mockContainer2.EXPECT().SetVisible(true).Once() | |||
| 661 | + | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |||
| 662 | + | ||||
| 663 | + | // Act | |||
| 664 | + | index := sv.InsertPaneAfter(ctx, 0, "Page 2", "", mockContainer2) | |||
| 665 | + | ||||
| 666 | + | // Assert | |||
| 667 | + | assert.Equal(t, 1, index) | |||
| 668 | + | assert.Equal(t, 2, sv.Count()) | |||
| 669 | + | assert.Equal(t, 1, sv.ActiveIndex()) | |||
| 670 | + | } | |||
| 671 | + | ||||
| 672 | + | func TestInsertPaneAfter_MaintainsOrder(t *testing.T) { | |||
| 673 | + | // Arrange | |||
| 674 | + | ctx := context.Background() | |||
| 675 | + | mockFactory, mockBox := setupMockFactory(t) | |||
| 676 | + | ||||
| 677 | + | // Setup 3 initial panes: A, B, C | |||
| 678 | + | containers := make([]*mocks.MockWidget, 3) | |||
| 679 | + | titleBars := make([]*mocks.MockBoxWidget, 3) | |||
| 680 | + | ||||
| 681 | + | for i := 0; i < 3; i++ { | |||
| 682 | + | titleBars[i], _, _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox) | |||
| 683 | + | titleBars[i].EXPECT().GetParent().Return(nil).Maybe() | |||
| 684 | + | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |||
| 685 | + | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |||
| 686 | + | titleBars[i].EXPECT().RemoveCssClass("active").Maybe() | |||
| 687 | + | } | |||
| 688 | + | ||||
| 689 | + | sv := layout.NewStackedView(mockFactory) | |||
| 690 | + | sv.AddPane(ctx, "A", "", containers[0]) // index 0 | |||
| 691 | + | sv.AddPane(ctx, "B", "", containers[1]) // index 1 | |||
| 692 | + | sv.AddPane(ctx, "C", "", containers[2]) // index 2 | |||
| 693 | + | ||||
| 694 | + | // Navigate to pane B (index 1) | |||
| 695 | + | require.NoError(t, sv.SetActive(ctx, 1)) | |||
| 696 | + | ||||
| 697 | + | // Insert D after B (should be at index 2, C moves to index 3) | |||
| 698 | + | mockTitleBar4, mockContainer4 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[1]) | |||
| 699 | + | mockTitleBar4.EXPECT().GetParent().Return(nil).Maybe() | |||
| 700 | + | mockContainer4.EXPECT().SetVisible(mock.Anything).Maybe() | |||
| 701 | + | mockTitleBar4.EXPECT().AddCssClass("active").Maybe() | |||
| 702 | + | mockTitleBar4.EXPECT().RemoveCssClass("active").Maybe() | |||
| 703 | + | ||||
| 704 | + | // Act | |||
| 705 | + | index := sv.InsertPaneAfter(ctx, 1, "D", "", mockContainer4) | |||
| 706 | + | ||||
| 707 | + | // Assert: Order should be A(0), B(1), D(2), C(3) | |||
| 708 | + | assert.Equal(t, 2, index) | |||
| 709 | + | assert.Equal(t, 4, sv.Count()) | |||
| 710 | + | assert.Equal(t, 2, sv.ActiveIndex()) // D is now active | |||
| 711 | + | ||||
| 712 | + | // Verify we can still get containers at expected indices | |||
| 713 | + | containerA, _ := sv.GetContainer(0) | |||
| 714 | + | containerB, _ := sv.GetContainer(1) | |||
| 715 | + | containerD, _ := sv.GetContainer(2) | |||
| 716 | + | containerC, _ := sv.GetContainer(3) | |||
| 717 | + | ||||
| 718 | + | assert.Equal(t, containers[0], containerA) | |||
| 719 | + | assert.Equal(t, containers[1], containerB) | |||
| 720 | + | assert.Equal(t, mockContainer4, containerD) | |||
| 721 | + | assert.Equal(t, containers[2], containerC) | |||
| 722 | + | } | |||
| 723 | + | ||||
| 724 | + | func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) { | |||
| 725 | + | // Arrange | |||
| 726 | + | ctx := context.Background() | |||
| 727 | + | mockFactory, mockBox := setupMockFactory(t) | |||
| 728 | + | ||||
| 729 | + | // First pane | |||
| 730 | + | mockTitleBar1, _, _, _, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |||
| 731 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |||
| 732 | + | mockContainer1.EXPECT().SetVisible(true).Once() | |||
| 733 | + | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |||
| 734 | + | ||||
| 735 | + | sv := layout.NewStackedView(mockFactory) | |||
| 736 | + | sv.AddPane(ctx, "Page 1", "", mockContainer1) | |||
| 737 | + | ||||
| 738 | + | // Try to insert at invalid index (100) - should clamp to end | |||
| 739 | + | mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1) | |||
| 740 | + | ||||
| 741 | + | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |||
| 742 | + | mockContainer1.EXPECT().SetVisible(false).Once() | |||
| 743 | + | mockTitleBar1.EXPECT().RemoveCssClass("active").Once() | |||
| 744 | + | ||||
| 745 | + | mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe() | |||
| 746 | + | mockContainer2.EXPECT().SetVisible(true).Once() | |||
| 747 | + | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |||
| 748 | + | ||||
| 749 | + | // Act - afterIndex=100 should be clamped to 0 (last valid index) | |||
| 750 | + | index := sv.InsertPaneAfter(ctx, 100, "Page 2", "", mockContainer2) | |||
| 751 | + | ||||
| 752 | + | // Assert | |||
| 753 | + | assert.Equal(t, 1, index) | |||
| 754 | + | assert.Equal(t, 2, sv.Count()) | |||
| 755 | + | } | |||