refactor(ui): extract workspace close context helper
- commit
- 0b6a0cf138920848bda8a4661e0bd3494516db3c
- author
- brice · · 2026-06-25 12:43 UTC
- parent
- dbc9709
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +150 -84
M internal/ui/coordinator/workspace.go +0 -84
| @@ -75,16 +75,6 @@ type stackPaneContext struct { | |||
| 75 | 75 | originalTitle string | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | - | type incrementalCloseContext struct { | |
| 79 | - | parentNode *entity.PaneNode | |
| 80 | - | siblingNode *entity.PaneNode | |
| 81 | - | grandparentNode *entity.PaneNode | |
| 82 | - | parentWidget layout.Widget | |
| 83 | - | siblingIsStartChild bool | |
| 84 | - | parentIsStartInGrand bool | |
| 85 | - | precheckReason string | |
| 86 | - | } | |
| 87 | - | ||
| 88 | 78 | const ( | |
| 89 | 79 | defaultResizeStepPercent = 0.05 | |
| 90 | 80 | defaultResizeMinPanePercent = 0.1 | |
| @@ -1261,80 +1251,6 @@ func (c *WorkspaceCoordinator) captureIncrementalCloseContext( | |||
| 1261 | 1251 | return closeCtx | |
| 1262 | 1252 | } | |
| 1263 | 1253 | ||
| 1264 | - | func deriveIncrementalCloseTreeContext(closingPane *entity.PaneNode) (incrementalCloseContext, error) { | |
| 1265 | - | var closeCtx incrementalCloseContext | |
| 1266 | - | if closingPane == nil { | |
| 1267 | - | return closeCtx, errors.New("closing pane missing") | |
| 1268 | - | } | |
| 1269 | - | ||
| 1270 | - | parentNode := closingPane.Parent | |
| 1271 | - | if parentNode == nil { | |
| 1272 | - | return closeCtx, errors.New("closing pane has no parent") | |
| 1273 | - | } | |
| 1274 | - | ||
| 1275 | - | closeCtx.parentNode = parentNode | |
| 1276 | - | closeCtx.grandparentNode = parentNode.Parent | |
| 1277 | - | ||
| 1278 | - | if parentNode.IsStacked { | |
| 1279 | - | return closeCtx, nil | |
| 1280 | - | } | |
| 1281 | - | ||
| 1282 | - | if !parentNode.IsSplit() { | |
| 1283 | - | return closeCtx, fmt.Errorf("parent node is not split: %s", parentNode.ID) | |
| 1284 | - | } | |
| 1285 | - | ||
| 1286 | - | if len(parentNode.Children) != 2 { | |
| 1287 | - | return closeCtx, fmt.Errorf("split parent has invalid child count: %d", len(parentNode.Children)) | |
| 1288 | - | } | |
| 1289 | - | ||
| 1290 | - | leftChild := parentNode.Left() | |
| 1291 | - | rightChild := parentNode.Right() | |
| 1292 | - | if leftChild == closingPane && rightChild == nil { | |
| 1293 | - | return closeCtx, errors.New("sibling missing") | |
| 1294 | - | } | |
| 1295 | - | if rightChild == closingPane && leftChild == nil { | |
| 1296 | - | return closeCtx, errors.New("sibling missing") | |
| 1297 | - | } | |
| 1298 | - | if leftChild == nil || rightChild == nil { | |
| 1299 | - | return closeCtx, errors.New("split parent has nil child") | |
| 1300 | - | } | |
| 1301 | - | ||
| 1302 | - | switch { | |
| 1303 | - | case leftChild == closingPane: | |
| 1304 | - | closeCtx.siblingNode = rightChild | |
| 1305 | - | closeCtx.siblingIsStartChild = false | |
| 1306 | - | case rightChild == closingPane: | |
| 1307 | - | closeCtx.siblingNode = leftChild | |
| 1308 | - | closeCtx.siblingIsStartChild = true | |
| 1309 | - | default: | |
| 1310 | - | return closeCtx, fmt.Errorf("closing pane not found under parent: %s", parentNode.ID) | |
| 1311 | - | } | |
| 1312 | - | ||
| 1313 | - | if closeCtx.siblingNode == nil { | |
| 1314 | - | return closeCtx, errors.New("sibling missing") | |
| 1315 | - | } | |
| 1316 | - | ||
| 1317 | - | if closeCtx.grandparentNode != nil { | |
| 1318 | - | switch { | |
| 1319 | - | case closeCtx.grandparentNode.Left() == parentNode: | |
| 1320 | - | closeCtx.parentIsStartInGrand = true | |
| 1321 | - | case closeCtx.grandparentNode.Right() == parentNode: | |
| 1322 | - | closeCtx.parentIsStartInGrand = false | |
| 1323 | - | default: | |
| 1324 | - | return closeCtx, fmt.Errorf("parent not found under grandparent: %s", closeCtx.grandparentNode.ID) | |
| 1325 | - | } | |
| 1326 | - | } | |
| 1327 | - | ||
| 1328 | - | return closeCtx, nil | |
| 1329 | - | } | |
| 1330 | - | ||
| 1331 | - | func paneNodeID(node *entity.PaneNode) string { | |
| 1332 | - | if node == nil { | |
| 1333 | - | return nilString | |
| 1334 | - | } | |
| 1335 | - | return node.ID | |
| 1336 | - | } | |
| 1337 | - | ||
| 1338 | 1254 | // doIncrementalStackClose removes a pane from a stack without rebuilding the entire tree. | |
| 1339 | 1255 | func (c *WorkspaceCoordinator) doIncrementalStackClose( | |
| 1340 | 1256 | ctx context.Context, | |
| @@ -75,16 +75,6 @@ type stackPaneContext struct { | |||||
| 75 | originalTitle string | 75 | originalTitle string | ||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | - | type incrementalCloseContext struct { | |||
| 79 | - | parentNode *entity.PaneNode | |||
| 80 | - | siblingNode *entity.PaneNode | |||
| 81 | - | grandparentNode *entity.PaneNode | |||
| 82 | - | parentWidget layout.Widget | |||
| 83 | - | siblingIsStartChild bool | |||
| 84 | - | parentIsStartInGrand bool | |||
| 85 | - | precheckReason string | |||
| 86 | - | } | |||
| 87 | - | ||||
| 88 | const ( | 78 | const ( | ||
| 89 | defaultResizeStepPercent = 0.05 | 79 | defaultResizeStepPercent = 0.05 | ||
| 90 | defaultResizeMinPanePercent = 0.1 | 80 | defaultResizeMinPanePercent = 0.1 | ||
| @@ -1261,80 +1251,6 @@ func (c *WorkspaceCoordinator) captureIncrementalCloseContext( | |||||
| 1261 | return closeCtx | 1251 | return closeCtx | ||
| 1262 | } | 1252 | } | ||
| 1263 | 1253 | ||||
| 1264 | - | func deriveIncrementalCloseTreeContext(closingPane *entity.PaneNode) (incrementalCloseContext, error) { | |||
| 1265 | - | var closeCtx incrementalCloseContext | |||
| 1266 | - | if closingPane == nil { | |||
| 1267 | - | return closeCtx, errors.New("closing pane missing") | |||
| 1268 | - | } | |||
| 1269 | - | ||||
| 1270 | - | parentNode := closingPane.Parent | |||
| 1271 | - | if parentNode == nil { | |||
| 1272 | - | return closeCtx, errors.New("closing pane has no parent") | |||
| 1273 | - | } | |||
| 1274 | - | ||||
| 1275 | - | closeCtx.parentNode = parentNode | |||
| 1276 | - | closeCtx.grandparentNode = parentNode.Parent | |||
| 1277 | - | ||||
| 1278 | - | if parentNode.IsStacked { | |||
| 1279 | - | return closeCtx, nil | |||
| 1280 | - | } | |||
| 1281 | - | ||||
| 1282 | - | if !parentNode.IsSplit() { | |||
| 1283 | - | return closeCtx, fmt.Errorf("parent node is not split: %s", parentNode.ID) | |||
| 1284 | - | } | |||
| 1285 | - | ||||
| 1286 | - | if len(parentNode.Children) != 2 { | |||
| 1287 | - | return closeCtx, fmt.Errorf("split parent has invalid child count: %d", len(parentNode.Children)) | |||
| 1288 | - | } | |||
| 1289 | - | ||||
| 1290 | - | leftChild := parentNode.Left() | |||
| 1291 | - | rightChild := parentNode.Right() | |||
| 1292 | - | if leftChild == closingPane && rightChild == nil { | |||
| 1293 | - | return closeCtx, errors.New("sibling missing") | |||
| 1294 | - | } | |||
| 1295 | - | if rightChild == closingPane && leftChild == nil { | |||
| 1296 | - | return closeCtx, errors.New("sibling missing") | |||
| 1297 | - | } | |||
| 1298 | - | if leftChild == nil || rightChild == nil { | |||
| 1299 | - | return closeCtx, errors.New("split parent has nil child") | |||
| 1300 | - | } | |||
| 1301 | - | ||||
| 1302 | - | switch { | |||
| 1303 | - | case leftChild == closingPane: | |||
| 1304 | - | closeCtx.siblingNode = rightChild | |||
| 1305 | - | closeCtx.siblingIsStartChild = false | |||
| 1306 | - | case rightChild == closingPane: | |||
| 1307 | - | closeCtx.siblingNode = leftChild | |||
| 1308 | - | closeCtx.siblingIsStartChild = true | |||
| 1309 | - | default: | |||
| 1310 | - | return closeCtx, fmt.Errorf("closing pane not found under parent: %s", parentNode.ID) | |||
| 1311 | - | } | |||
| 1312 | - | ||||
| 1313 | - | if closeCtx.siblingNode == nil { | |||
| 1314 | - | return closeCtx, errors.New("sibling missing") | |||
| 1315 | - | } | |||
| 1316 | - | ||||
| 1317 | - | if closeCtx.grandparentNode != nil { | |||
| 1318 | - | switch { | |||
| 1319 | - | case closeCtx.grandparentNode.Left() == parentNode: | |||
| 1320 | - | closeCtx.parentIsStartInGrand = true | |||
| 1321 | - | case closeCtx.grandparentNode.Right() == parentNode: | |||
| 1322 | - | closeCtx.parentIsStartInGrand = false | |||
| 1323 | - | default: | |||
| 1324 | - | return closeCtx, fmt.Errorf("parent not found under grandparent: %s", closeCtx.grandparentNode.ID) | |||
| 1325 | - | } | |||
| 1326 | - | } | |||
| 1327 | - | ||||
| 1328 | - | return closeCtx, nil | |||
| 1329 | - | } | |||
| 1330 | - | ||||
| 1331 | - | func paneNodeID(node *entity.PaneNode) string { | |||
| 1332 | - | if node == nil { | |||
| 1333 | - | return nilString | |||
| 1334 | - | } | |||
| 1335 | - | return node.ID | |||
| 1336 | - | } | |||
| 1337 | - | ||||
| 1338 | // doIncrementalStackClose removes a pane from a stack without rebuilding the entire tree. | 1254 | // doIncrementalStackClose removes a pane from a stack without rebuilding the entire tree. | ||
| 1339 | func (c *WorkspaceCoordinator) doIncrementalStackClose( | 1255 | func (c *WorkspaceCoordinator) doIncrementalStackClose( | ||
| 1340 | ctx context.Context, | 1256 | ctx context.Context, | ||
A internal/ui/coordinator/workspace_close_context.go +93 -0
| @@ -0,0 +1,93 @@ | |||
| 1 | + | package coordinator | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 8 | + | "github.com/bnema/dumber/internal/ui/layout" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | type incrementalCloseContext struct { | |
| 12 | + | parentNode *entity.PaneNode | |
| 13 | + | siblingNode *entity.PaneNode | |
| 14 | + | grandparentNode *entity.PaneNode | |
| 15 | + | parentWidget layout.Widget | |
| 16 | + | siblingIsStartChild bool | |
| 17 | + | parentIsStartInGrand bool | |
| 18 | + | precheckReason string | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func deriveIncrementalCloseTreeContext(closingPane *entity.PaneNode) (incrementalCloseContext, error) { | |
| 22 | + | var closeCtx incrementalCloseContext | |
| 23 | + | if closingPane == nil { | |
| 24 | + | return closeCtx, errors.New("closing pane missing") | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | parentNode := closingPane.Parent | |
| 28 | + | if parentNode == nil { | |
| 29 | + | return closeCtx, errors.New("closing pane has no parent") | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | closeCtx.parentNode = parentNode | |
| 33 | + | closeCtx.grandparentNode = parentNode.Parent | |
| 34 | + | ||
| 35 | + | if parentNode.IsStacked { | |
| 36 | + | return closeCtx, nil | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | if !parentNode.IsSplit() { | |
| 40 | + | return closeCtx, fmt.Errorf("parent node is not split: %s", parentNode.ID) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | if len(parentNode.Children) != 2 { | |
| 44 | + | return closeCtx, fmt.Errorf("split parent has invalid child count: %d", len(parentNode.Children)) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | leftChild := parentNode.Left() | |
| 48 | + | rightChild := parentNode.Right() | |
| 49 | + | if leftChild == closingPane && rightChild == nil { | |
| 50 | + | return closeCtx, errors.New("sibling missing") | |
| 51 | + | } | |
| 52 | + | if rightChild == closingPane && leftChild == nil { | |
| 53 | + | return closeCtx, errors.New("sibling missing") | |
| 54 | + | } | |
| 55 | + | if leftChild == nil || rightChild == nil { | |
| 56 | + | return closeCtx, errors.New("split parent has nil child") | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | switch { | |
| 60 | + | case leftChild == closingPane: | |
| 61 | + | closeCtx.siblingNode = rightChild | |
| 62 | + | closeCtx.siblingIsStartChild = false | |
| 63 | + | case rightChild == closingPane: | |
| 64 | + | closeCtx.siblingNode = leftChild | |
| 65 | + | closeCtx.siblingIsStartChild = true | |
| 66 | + | default: | |
| 67 | + | return closeCtx, fmt.Errorf("closing pane not found under parent: %s", parentNode.ID) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | if closeCtx.siblingNode == nil { | |
| 71 | + | return closeCtx, errors.New("sibling missing") | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | if closeCtx.grandparentNode != nil { | |
| 75 | + | switch { | |
| 76 | + | case closeCtx.grandparentNode.Left() == parentNode: | |
| 77 | + | closeCtx.parentIsStartInGrand = true | |
| 78 | + | case closeCtx.grandparentNode.Right() == parentNode: | |
| 79 | + | closeCtx.parentIsStartInGrand = false | |
| 80 | + | default: | |
| 81 | + | return closeCtx, fmt.Errorf("parent not found under grandparent: %s", closeCtx.grandparentNode.ID) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | return closeCtx, nil | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func paneNodeID(node *entity.PaneNode) string { | |
| 89 | + | if node == nil { | |
| 90 | + | return nilString | |
| 91 | + | } | |
| 92 | + | return node.ID | |
| 93 | + | } | |
| @@ -0,0 +1,93 @@ | |||||
| 1 | + | package coordinator | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 8 | + | "github.com/bnema/dumber/internal/ui/layout" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | type incrementalCloseContext struct { | |||
| 12 | + | parentNode *entity.PaneNode | |||
| 13 | + | siblingNode *entity.PaneNode | |||
| 14 | + | grandparentNode *entity.PaneNode | |||
| 15 | + | parentWidget layout.Widget | |||
| 16 | + | siblingIsStartChild bool | |||
| 17 | + | parentIsStartInGrand bool | |||
| 18 | + | precheckReason string | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func deriveIncrementalCloseTreeContext(closingPane *entity.PaneNode) (incrementalCloseContext, error) { | |||
| 22 | + | var closeCtx incrementalCloseContext | |||
| 23 | + | if closingPane == nil { | |||
| 24 | + | return closeCtx, errors.New("closing pane missing") | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | parentNode := closingPane.Parent | |||
| 28 | + | if parentNode == nil { | |||
| 29 | + | return closeCtx, errors.New("closing pane has no parent") | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | closeCtx.parentNode = parentNode | |||
| 33 | + | closeCtx.grandparentNode = parentNode.Parent | |||
| 34 | + | ||||
| 35 | + | if parentNode.IsStacked { | |||
| 36 | + | return closeCtx, nil | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | if !parentNode.IsSplit() { | |||
| 40 | + | return closeCtx, fmt.Errorf("parent node is not split: %s", parentNode.ID) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | if len(parentNode.Children) != 2 { | |||
| 44 | + | return closeCtx, fmt.Errorf("split parent has invalid child count: %d", len(parentNode.Children)) | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | leftChild := parentNode.Left() | |||
| 48 | + | rightChild := parentNode.Right() | |||
| 49 | + | if leftChild == closingPane && rightChild == nil { | |||
| 50 | + | return closeCtx, errors.New("sibling missing") | |||
| 51 | + | } | |||
| 52 | + | if rightChild == closingPane && leftChild == nil { | |||
| 53 | + | return closeCtx, errors.New("sibling missing") | |||
| 54 | + | } | |||
| 55 | + | if leftChild == nil || rightChild == nil { | |||
| 56 | + | return closeCtx, errors.New("split parent has nil child") | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | switch { | |||
| 60 | + | case leftChild == closingPane: | |||
| 61 | + | closeCtx.siblingNode = rightChild | |||
| 62 | + | closeCtx.siblingIsStartChild = false | |||
| 63 | + | case rightChild == closingPane: | |||
| 64 | + | closeCtx.siblingNode = leftChild | |||
| 65 | + | closeCtx.siblingIsStartChild = true | |||
| 66 | + | default: | |||
| 67 | + | return closeCtx, fmt.Errorf("closing pane not found under parent: %s", parentNode.ID) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | if closeCtx.siblingNode == nil { | |||
| 71 | + | return closeCtx, errors.New("sibling missing") | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | if closeCtx.grandparentNode != nil { | |||
| 75 | + | switch { | |||
| 76 | + | case closeCtx.grandparentNode.Left() == parentNode: | |||
| 77 | + | closeCtx.parentIsStartInGrand = true | |||
| 78 | + | case closeCtx.grandparentNode.Right() == parentNode: | |||
| 79 | + | closeCtx.parentIsStartInGrand = false | |||
| 80 | + | default: | |||
| 81 | + | return closeCtx, fmt.Errorf("parent not found under grandparent: %s", closeCtx.grandparentNode.ID) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | return closeCtx, nil | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | func paneNodeID(node *entity.PaneNode) string { | |||
| 89 | + | if node == nil { | |||
| 90 | + | return nilString | |||
| 91 | + | } | |||
| 92 | + | return node.ID | |||
| 93 | + | } | |||
M internal/ui/coordinator/workspace_close_test.go +57 -0
| @@ -59,6 +59,47 @@ func TestDeriveIncrementalCloseTreeContext_ValidSplit(t *testing.T) { | |||
| 59 | 59 | } | |
| 60 | 60 | } | |
| 61 | 61 | ||
| 62 | + | func TestDeriveIncrementalCloseTreeContext_InvalidPreconditions(t *testing.T) { | |
| 63 | + | tests := []struct { | |
| 64 | + | name string | |
| 65 | + | closingPane *entity.PaneNode | |
| 66 | + | wantText string | |
| 67 | + | }{ | |
| 68 | + | { | |
| 69 | + | name: "nil closing pane", | |
| 70 | + | closingPane: nil, | |
| 71 | + | wantText: "closing pane missing", | |
| 72 | + | }, | |
| 73 | + | { | |
| 74 | + | name: "closing pane has no parent", | |
| 75 | + | closingPane: testLeafNode("closing"), | |
| 76 | + | wantText: "closing pane has no parent", | |
| 77 | + | }, | |
| 78 | + | { | |
| 79 | + | name: "parent is not split", | |
| 80 | + | closingPane: func() *entity.PaneNode { | |
| 81 | + | closing := testLeafNode("closing") | |
| 82 | + | parent := &entity.PaneNode{ID: "parent", Children: []*entity.PaneNode{closing}} | |
| 83 | + | closing.Parent = parent | |
| 84 | + | return closing | |
| 85 | + | }(), | |
| 86 | + | wantText: "parent node is not split", | |
| 87 | + | }, | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | for _, tt := range tests { | |
| 91 | + | t.Run(tt.name, func(t *testing.T) { | |
| 92 | + | _, err := deriveIncrementalCloseTreeContext(tt.closingPane) | |
| 93 | + | if err == nil { | |
| 94 | + | t.Fatalf("expected error") | |
| 95 | + | } | |
| 96 | + | if !strings.Contains(err.Error(), tt.wantText) { | |
| 97 | + | t.Fatalf("expected %q error, got: %v", tt.wantText, err) | |
| 98 | + | } | |
| 99 | + | }) | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | ||
| 62 | 103 | func TestDeriveIncrementalCloseTreeContext_MissingSibling(t *testing.T) { | |
| 63 | 104 | closing := testLeafNode("closing") | |
| 64 | 105 | parent := testSplitNode("parent", closing, nil) | |
| @@ -92,6 +133,22 @@ func TestDeriveIncrementalCloseTreeContext_ParentDoesNotContainClosingPane(t *te | |||
| 92 | 133 | } | |
| 93 | 134 | } | |
| 94 | 135 | ||
| 136 | + | func TestDeriveIncrementalCloseTreeContext_ParentMissingFromGrandparent(t *testing.T) { | |
| 137 | + | closing := testLeafNode("closing") | |
| 138 | + | sibling := testLeafNode("sibling") | |
| 139 | + | parent := testSplitNode("parent", closing, sibling) | |
| 140 | + | grand := testSplitNode("grand", testLeafNode("other-left"), testLeafNode("other-right")) | |
| 141 | + | parent.Parent = grand | |
| 142 | + | ||
| 143 | + | _, err := deriveIncrementalCloseTreeContext(closing) | |
| 144 | + | if err == nil { | |
| 145 | + | t.Fatalf("expected invariant error") | |
| 146 | + | } | |
| 147 | + | if !strings.Contains(err.Error(), "parent not found under grandparent") { | |
| 148 | + | t.Fatalf("unexpected error: %v", err) | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | ||
| 95 | 152 | func TestCaptureIncrementalCloseContext_MissingSiblingSetsPrecheckReason(t *testing.T) { | |
| 96 | 153 | closing := testLeafNode("closing") | |
| 97 | 154 | testSplitNode("parent", closing, nil) | |
| @@ -59,6 +59,47 @@ func TestDeriveIncrementalCloseTreeContext_ValidSplit(t *testing.T) { | |||||
| 59 | } | 59 | } | ||
| 60 | } | 60 | } | ||
| 61 | 61 | ||||
| 62 | + | func TestDeriveIncrementalCloseTreeContext_InvalidPreconditions(t *testing.T) { | |||
| 63 | + | tests := []struct { | |||
| 64 | + | name string | |||
| 65 | + | closingPane *entity.PaneNode | |||
| 66 | + | wantText string | |||
| 67 | + | }{ | |||
| 68 | + | { | |||
| 69 | + | name: "nil closing pane", | |||
| 70 | + | closingPane: nil, | |||
| 71 | + | wantText: "closing pane missing", | |||
| 72 | + | }, | |||
| 73 | + | { | |||
| 74 | + | name: "closing pane has no parent", | |||
| 75 | + | closingPane: testLeafNode("closing"), | |||
| 76 | + | wantText: "closing pane has no parent", | |||
| 77 | + | }, | |||
| 78 | + | { | |||
| 79 | + | name: "parent is not split", | |||
| 80 | + | closingPane: func() *entity.PaneNode { | |||
| 81 | + | closing := testLeafNode("closing") | |||
| 82 | + | parent := &entity.PaneNode{ID: "parent", Children: []*entity.PaneNode{closing}} | |||
| 83 | + | closing.Parent = parent | |||
| 84 | + | return closing | |||
| 85 | + | }(), | |||
| 86 | + | wantText: "parent node is not split", | |||
| 87 | + | }, | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | for _, tt := range tests { | |||
| 91 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 92 | + | _, err := deriveIncrementalCloseTreeContext(tt.closingPane) | |||
| 93 | + | if err == nil { | |||
| 94 | + | t.Fatalf("expected error") | |||
| 95 | + | } | |||
| 96 | + | if !strings.Contains(err.Error(), tt.wantText) { | |||
| 97 | + | t.Fatalf("expected %q error, got: %v", tt.wantText, err) | |||
| 98 | + | } | |||
| 99 | + | }) | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | ||||
| 62 | func TestDeriveIncrementalCloseTreeContext_MissingSibling(t *testing.T) { | 103 | func TestDeriveIncrementalCloseTreeContext_MissingSibling(t *testing.T) { | ||
| 63 | closing := testLeafNode("closing") | 104 | closing := testLeafNode("closing") | ||
| 64 | parent := testSplitNode("parent", closing, nil) | 105 | parent := testSplitNode("parent", closing, nil) | ||
| @@ -92,6 +133,22 @@ func TestDeriveIncrementalCloseTreeContext_ParentDoesNotContainClosingPane(t *te | |||||
| 92 | } | 133 | } | ||
| 93 | } | 134 | } | ||
| 94 | 135 | ||||
| 136 | + | func TestDeriveIncrementalCloseTreeContext_ParentMissingFromGrandparent(t *testing.T) { | |||
| 137 | + | closing := testLeafNode("closing") | |||
| 138 | + | sibling := testLeafNode("sibling") | |||
| 139 | + | parent := testSplitNode("parent", closing, sibling) | |||
| 140 | + | grand := testSplitNode("grand", testLeafNode("other-left"), testLeafNode("other-right")) | |||
| 141 | + | parent.Parent = grand | |||
| 142 | + | ||||
| 143 | + | _, err := deriveIncrementalCloseTreeContext(closing) | |||
| 144 | + | if err == nil { | |||
| 145 | + | t.Fatalf("expected invariant error") | |||
| 146 | + | } | |||
| 147 | + | if !strings.Contains(err.Error(), "parent not found under grandparent") { | |||
| 148 | + | t.Fatalf("unexpected error: %v", err) | |||
| 149 | + | } | |||
| 150 | + | } | |||
| 151 | + | ||||
| 95 | func TestCaptureIncrementalCloseContext_MissingSiblingSetsPrecheckReason(t *testing.T) { | 152 | func TestCaptureIncrementalCloseContext_MissingSiblingSetsPrecheckReason(t *testing.T) { | ||
| 96 | closing := testLeafNode("closing") | 153 | closing := testLeafNode("closing") | ||
| 97 | testSplitNode("parent", closing, nil) | 154 | testSplitNode("parent", closing, nil) | ||