Guard nil workspace pane views
- commit
- e97fa5f7852af8ce02751f6a3b68ac68c8ed7134
- author
- brice · · 2026-05-11 18:35 UTC
- parent
- 9e90b30
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +38 -6
M internal/ui/component/workspace_view.go +6 -6
| @@ -252,7 +252,7 @@ func (wv *WorkspaceView) restoreActivePaneInternal() { | |||
| 252 | 252 | ||
| 253 | 253 | paneID := wv.workspace.ActivePaneID | |
| 254 | 254 | if paneID != "" { | |
| 255 | - | if _, ok := wv.paneViews[paneID]; ok { | |
| 255 | + | if pv, ok := wv.paneViews[paneID]; ok && pv != nil { | |
| 256 | 256 | _ = wv.setActivePaneIDInternal(paneID) | |
| 257 | 257 | return | |
| 258 | 258 | } | |
| @@ -278,8 +278,8 @@ func (wv *WorkspaceView) firstPaneIDInNodeInternal(node *entity.PaneNode) (entit | |||
| 278 | 278 | } | |
| 279 | 279 | if node.Pane != nil { | |
| 280 | 280 | paneID := node.Pane.ID | |
| 281 | - | _, ok := wv.paneViews[paneID] | |
| 282 | - | return paneID, ok | |
| 281 | + | pv, ok := wv.paneViews[paneID] | |
| 282 | + | return paneID, ok && pv != nil | |
| 283 | 283 | } | |
| 284 | 284 | for _, child := range node.Children { | |
| 285 | 285 | if paneID, ok := wv.firstPaneIDInNodeInternal(child); ok { | |
| @@ -302,7 +302,7 @@ func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | |||
| 302 | 302 | // Validate the target pane before mutating UI state. Invalid/stale IDs | |
| 303 | 303 | // should leave the current active pane styling and overlays untouched. | |
| 304 | 304 | newPV, ok := wv.paneViews[paneID] | |
| 305 | - | if !ok { | |
| 305 | + | if !ok || newPV == nil { | |
| 306 | 306 | return ErrPaneNotFound | |
| 307 | 307 | } | |
| 308 | 308 | ||
| @@ -318,7 +318,7 @@ func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | |||
| 318 | 318 | ||
| 319 | 319 | // Deactivate current active pane | |
| 320 | 320 | if currentActiveID != "" { | |
| 321 | - | if oldPV, ok := wv.paneViews[currentActiveID]; ok { | |
| 321 | + | if oldPV, ok := wv.paneViews[currentActiveID]; ok && oldPV != nil { | |
| 322 | 322 | oldPV.SetActive(false) | |
| 323 | 323 | } | |
| 324 | 324 | } | |
| @@ -481,7 +481,7 @@ func (wv *WorkspaceView) SetWebViewWidget(paneID entity.PaneID, widget layout.Wi | |||
| 481 | 481 | callback := wv.onWebViewAttached | |
| 482 | 482 | wv.mu.RUnlock() | |
| 483 | 483 | ||
| 484 | - | if !ok { | |
| 484 | + | if !ok || pv == nil { | |
| 485 | 485 | return ErrPaneNotFound | |
| 486 | 486 | } | |
| 487 | 487 | ||
| @@ -252,7 +252,7 @@ func (wv *WorkspaceView) restoreActivePaneInternal() { | |||||
| 252 | 252 | ||||
| 253 | paneID := wv.workspace.ActivePaneID | 253 | paneID := wv.workspace.ActivePaneID | ||
| 254 | if paneID != "" { | 254 | if paneID != "" { | ||
| 255 | - | if _, ok := wv.paneViews[paneID]; ok { | 255 | + | if pv, ok := wv.paneViews[paneID]; ok && pv != nil { |
| 256 | _ = wv.setActivePaneIDInternal(paneID) | 256 | _ = wv.setActivePaneIDInternal(paneID) | ||
| 257 | return | 257 | return | ||
| 258 | } | 258 | } | ||
| @@ -278,8 +278,8 @@ func (wv *WorkspaceView) firstPaneIDInNodeInternal(node *entity.PaneNode) (entit | |||||
| 278 | } | 278 | } | ||
| 279 | if node.Pane != nil { | 279 | if node.Pane != nil { | ||
| 280 | paneID := node.Pane.ID | 280 | paneID := node.Pane.ID | ||
| 281 | - | _, ok := wv.paneViews[paneID] | 281 | + | pv, ok := wv.paneViews[paneID] |
| 282 | - | return paneID, ok | 282 | + | return paneID, ok && pv != nil |
| 283 | } | 283 | } | ||
| 284 | for _, child := range node.Children { | 284 | for _, child := range node.Children { | ||
| 285 | if paneID, ok := wv.firstPaneIDInNodeInternal(child); ok { | 285 | if paneID, ok := wv.firstPaneIDInNodeInternal(child); ok { | ||
| @@ -302,7 +302,7 @@ func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | |||||
| 302 | // Validate the target pane before mutating UI state. Invalid/stale IDs | 302 | // Validate the target pane before mutating UI state. Invalid/stale IDs | ||
| 303 | // should leave the current active pane styling and overlays untouched. | 303 | // should leave the current active pane styling and overlays untouched. | ||
| 304 | newPV, ok := wv.paneViews[paneID] | 304 | newPV, ok := wv.paneViews[paneID] | ||
| 305 | - | if !ok { | 305 | + | if !ok || newPV == nil { |
| 306 | return ErrPaneNotFound | 306 | return ErrPaneNotFound | ||
| 307 | } | 307 | } | ||
| 308 | 308 | ||||
| @@ -318,7 +318,7 @@ func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | |||||
| 318 | 318 | ||||
| 319 | // Deactivate current active pane | 319 | // Deactivate current active pane | ||
| 320 | if currentActiveID != "" { | 320 | if currentActiveID != "" { | ||
| 321 | - | if oldPV, ok := wv.paneViews[currentActiveID]; ok { | 321 | + | if oldPV, ok := wv.paneViews[currentActiveID]; ok && oldPV != nil { |
| 322 | oldPV.SetActive(false) | 322 | oldPV.SetActive(false) | ||
| 323 | } | 323 | } | ||
| 324 | } | 324 | } | ||
| @@ -481,7 +481,7 @@ func (wv *WorkspaceView) SetWebViewWidget(paneID entity.PaneID, widget layout.Wi | |||||
| 481 | callback := wv.onWebViewAttached | 481 | callback := wv.onWebViewAttached | ||
| 482 | wv.mu.RUnlock() | 482 | wv.mu.RUnlock() | ||
| 483 | 483 | ||||
| 484 | - | if !ok { | 484 | + | if !ok || pv == nil { |
| 485 | return ErrPaneNotFound | 485 | return ErrPaneNotFound | ||
| 486 | } | 486 | } | ||
| 487 | 487 | ||||
M internal/ui/component/workspace_view_test.go +32 -0
| @@ -530,6 +530,38 @@ func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) { | |||
| 530 | 530 | assert.ErrorIs(t, err, component.ErrPaneNotFound) | |
| 531 | 531 | } | |
| 532 | 532 | ||
| 533 | + | func TestSetActivePaneID_NilRegisteredPaneView_ReturnsError(t *testing.T) { | |
| 534 | + | // Arrange | |
| 535 | + | mockFactory := mocks.NewMockWidgetFactory(t) | |
| 536 | + | ctx, mockBox := setupWorkspaceViewBase(t, mockFactory) | |
| 537 | + | ||
| 538 | + | wv := component.NewWorkspaceView(ctx, mockFactory) | |
| 539 | + | mockBox.EXPECT().AddCssClass("single-pane").Once() | |
| 540 | + | wv.RegisterPaneView(entity.PaneID("pane-1"), nil) | |
| 541 | + | ||
| 542 | + | // Act | |
| 543 | + | err := wv.SetActivePaneID(entity.PaneID("pane-1")) | |
| 544 | + | ||
| 545 | + | // Assert | |
| 546 | + | require.ErrorIs(t, err, component.ErrPaneNotFound) | |
| 547 | + | } | |
| 548 | + | ||
| 549 | + | func TestSetWebViewWidget_NilRegisteredPaneView_ReturnsError(t *testing.T) { | |
| 550 | + | // Arrange | |
| 551 | + | mockFactory := mocks.NewMockWidgetFactory(t) | |
| 552 | + | ctx, mockBox := setupWorkspaceViewBase(t, mockFactory) | |
| 553 | + | ||
| 554 | + | wv := component.NewWorkspaceView(ctx, mockFactory) | |
| 555 | + | mockBox.EXPECT().AddCssClass("single-pane").Once() | |
| 556 | + | wv.RegisterPaneView(entity.PaneID("pane-1"), nil) | |
| 557 | + | ||
| 558 | + | // Act | |
| 559 | + | err := wv.SetWebViewWidget(entity.PaneID("pane-1"), mocks.NewMockWidget(t)) | |
| 560 | + | ||
| 561 | + | // Assert | |
| 562 | + | require.ErrorIs(t, err, component.ErrPaneNotFound) | |
| 563 | + | } | |
| 564 | + | ||
| 533 | 565 | func TestSetActivePaneID_InvalidID_PreservesCurrentActivePane(t *testing.T) { | |
| 534 | 566 | // Arrange | |
| 535 | 567 | mockFactory := mocks.NewMockWidgetFactory(t) | |
| @@ -530,6 +530,38 @@ func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) { | |||||
| 530 | assert.ErrorIs(t, err, component.ErrPaneNotFound) | 530 | assert.ErrorIs(t, err, component.ErrPaneNotFound) | ||
| 531 | } | 531 | } | ||
| 532 | 532 | ||||
| 533 | + | func TestSetActivePaneID_NilRegisteredPaneView_ReturnsError(t *testing.T) { | |||
| 534 | + | // Arrange | |||
| 535 | + | mockFactory := mocks.NewMockWidgetFactory(t) | |||
| 536 | + | ctx, mockBox := setupWorkspaceViewBase(t, mockFactory) | |||
| 537 | + | ||||
| 538 | + | wv := component.NewWorkspaceView(ctx, mockFactory) | |||
| 539 | + | mockBox.EXPECT().AddCssClass("single-pane").Once() | |||
| 540 | + | wv.RegisterPaneView(entity.PaneID("pane-1"), nil) | |||
| 541 | + | ||||
| 542 | + | // Act | |||
| 543 | + | err := wv.SetActivePaneID(entity.PaneID("pane-1")) | |||
| 544 | + | ||||
| 545 | + | // Assert | |||
| 546 | + | require.ErrorIs(t, err, component.ErrPaneNotFound) | |||
| 547 | + | } | |||
| 548 | + | ||||
| 549 | + | func TestSetWebViewWidget_NilRegisteredPaneView_ReturnsError(t *testing.T) { | |||
| 550 | + | // Arrange | |||
| 551 | + | mockFactory := mocks.NewMockWidgetFactory(t) | |||
| 552 | + | ctx, mockBox := setupWorkspaceViewBase(t, mockFactory) | |||
| 553 | + | ||||
| 554 | + | wv := component.NewWorkspaceView(ctx, mockFactory) | |||
| 555 | + | mockBox.EXPECT().AddCssClass("single-pane").Once() | |||
| 556 | + | wv.RegisterPaneView(entity.PaneID("pane-1"), nil) | |||
| 557 | + | ||||
| 558 | + | // Act | |||
| 559 | + | err := wv.SetWebViewWidget(entity.PaneID("pane-1"), mocks.NewMockWidget(t)) | |||
| 560 | + | ||||
| 561 | + | // Assert | |||
| 562 | + | require.ErrorIs(t, err, component.ErrPaneNotFound) | |||
| 563 | + | } | |||
| 564 | + | ||||
| 533 | func TestSetActivePaneID_InvalidID_PreservesCurrentActivePane(t *testing.T) { | 565 | func TestSetActivePaneID_InvalidID_PreservesCurrentActivePane(t *testing.T) { | ||
| 534 | // Arrange | 566 | // Arrange | ||
| 535 | mockFactory := mocks.NewMockWidgetFactory(t) | 567 | mockFactory := mocks.NewMockWidgetFactory(t) | ||