1package browser
2
3import (
4 "fmt"
5 "testing"
6 "time"
7
8 "github.com/bnema/dumber/pkg/webkit"
9)
10
11// Test helper to create mock pane node
12func newMockPaneNode(id string) *paneNode {
13 container := uintptr(100 + len(id))
14
15 return &paneNode{
16 pane: &BrowserPane{id: id},
17 container: container,
18 isLeaf: true,
19 }
20}
21
22// Test tree validation helper
23func validateTreeStructure(t *testing.T, node *paneNode) {
24 t.Helper()
25
26 if node == nil {
27 return
28 }
29
30 if node.isLeaf {
31 // Leaf nodes should not have children
32 if node.left != nil || node.right != nil {
33 t.Errorf("Leaf node has children: left=%v, right=%v", node.left, node.right)
34 }
35 // Leaf nodes must have a pane
36 if node.pane == nil {
37 t.Error("Leaf node missing pane")
38 }
39 } else {
40 // Branch nodes must have both children
41 if node.left == nil || node.right == nil {
42 t.Errorf("Branch node missing children: left=%v, right=%v", node.left, node.right)
43 }
44 // Branch nodes should not have a pane
45 if node.pane != nil {
46 t.Error("Branch node should not have pane")
47 }
48 // Validate children's parent pointers
49 if node.left != nil && node.left.parent != node {
50 t.Error("Left child's parent pointer incorrect")
51 }
52 if node.right != nil && node.right.parent != node {
53 t.Error("Right child's parent pointer incorrect")
54 }
55 // Recursively validate subtrees
56 validateTreeStructure(t, node.left)
57 validateTreeStructure(t, node.right)
58 }
59}
60
61// Count nodes in tree
62func countNodes(node *paneNode) (leaves, branches int) {
63 if node == nil {
64 return 0, 0
65 }
66 if node.isLeaf {
67 return 1, 0
68 }
69 leftLeaves, leftBranches := countNodes(node.left)
70 rightLeaves, rightBranches := countNodes(node.right)
71 return leftLeaves + rightLeaves, leftBranches + rightBranches + 1
72}
73
74func TestPaneNodeTreeInvariants(t *testing.T) {
75 t.Run("Initial root is leaf", func(t *testing.T) {
76 wm := newTestWorkspaceManagerWithMocksForTree(t)
77
78 if !wm.root.isLeaf {
79 t.Error("Initial root should be a leaf")
80 }
81 if wm.root.parent != nil {
82 t.Error("Root should have no parent")
83 }
84 validateTreeStructure(t, wm.root)
85 })
86
87 t.Run("Tree structure after single split", func(t *testing.T) {
88 wm := newTestWorkspaceManagerWithMocksForTree(t)
89 original := wm.root
90
91 // Split right
92 newNode, err := wm.splitNode(original, "right")
93 if err != nil {
94 t.Fatalf("Split failed: %v", err)
95 }
96
97 // Validate new tree structure
98 validateTreeStructure(t, wm.root)
99
100 // Root should now be a branch
101 if wm.root.isLeaf {
102 t.Error("Root should be branch after split")
103 }
104
105 // Original and new nodes should be siblings
106 if wm.root.left != original {
107 t.Error("Original node should be left child")
108 }
109 if wm.root.right != newNode {
110 t.Error("New node should be right child")
111 }
112
113 // Count nodes
114 leaves, branches := countNodes(wm.root)
115 if leaves != 2 {
116 t.Errorf("Expected 2 leaves, got %d", leaves)
117 }
118 if branches != 1 {
119 t.Errorf("Expected 1 branch, got %d", branches)
120 }
121 })
122}
123
124func TestPaneSplitDirections(t *testing.T) {
125 testCases := []struct {
126 name string
127 direction string
128 expectedOrientation webkit.Orientation
129 expectedOriginalPosition string // "left" or "right" child
130 }{
131 {"SplitRight", "right", webkit.OrientationHorizontal, "left"},
132 {"SplitLeft", "left", webkit.OrientationHorizontal, "right"},
133 {"SplitDown", "down", webkit.OrientationVertical, "left"},
134 {"SplitUp", "up", webkit.OrientationVertical, "right"},
135 }
136
137 for _, tc := range testCases {
138 t.Run(tc.name, func(t *testing.T) {
139 wm := newTestWorkspaceManagerWithMocksForTree(t)
140 original := wm.root
141
142 newNode, err := wm.splitNode(original, tc.direction)
143 if err != nil {
144 t.Fatalf("Split failed: %v", err)
145 }
146
147 parent := original.parent
148 if parent.orientation != tc.expectedOrientation {
149 t.Errorf("Expected orientation %v, got %v",
150 tc.expectedOrientation, parent.orientation)
151 }
152
153 if tc.expectedOriginalPosition == "left" {
154 if parent.left != original {
155 t.Error("Original should be left child")
156 }
157 if parent.right != newNode {
158 t.Error("New node should be right child")
159 }
160 } else {
161 if parent.right != original {
162 t.Error("Original should be right child")
163 }
164 if parent.left != newNode {
165 t.Error("New node should be left child")
166 }
167 }
168
169 validateTreeStructure(t, wm.root)
170 })
171 }
172}
173
174func TestComplexTreeOperations(t *testing.T) {
175 t.Run("Multiple nested splits", func(t *testing.T) {
176 wm := newTestWorkspaceManagerWithMocksForTree(t)
177
178 // Create complex tree: split right, then split the right pane down
179 node1, _ := wm.splitNode(wm.root, "right")
180 node2, _ := wm.splitNode(node1, "down")
181
182 validateTreeStructure(t, wm.root)
183
184 // Count nodes
185 leaves, branches := countNodes(wm.root)
186 if leaves != 3 {
187 t.Errorf("Expected 3 leaves, got %d", leaves)
188 }
189 if branches != 2 {
190 t.Errorf("Expected 2 branches, got %d", branches)
191 }
192
193 // Verify node2 is correctly positioned
194 if node2 == nil {
195 t.Error("node2 should not be nil")
196 }
197 })
198
199 t.Run("Deep nesting", func(t *testing.T) {
200 wm := newTestWorkspaceManagerWithMocksForTree(t)
201
202 // Create a chain of splits
203 current := wm.root
204 for i := 0; i < 5; i++ {
205 newNode, err := wm.splitNode(current, "right")
206 if err != nil {
207 t.Fatalf("Split %d failed: %v", i, err)
208 }
209 current = newNode
210 }
211
212 validateTreeStructure(t, wm.root)
213
214 leaves, branches := countNodes(wm.root)
215 if leaves != 6 {
216 t.Errorf("Expected 6 leaves, got %d", leaves)
217 }
218 if branches != 5 {
219 t.Errorf("Expected 5 branches, got %d", branches)
220 }
221 })
222}
223
224func TestPaneDeletion(t *testing.T) {
225 t.Run("Close leaf promotes sibling", func(t *testing.T) {
226 wm := newTestWorkspaceManagerWithMocksForTree(t)
227 original := wm.root
228
229 // Split and then close the new pane
230 newNode, _ := wm.splitNode(original, "right")
231
232 err := wm.closePane(newNode)
233 if err != nil {
234 t.Fatalf("Close failed: %v", err)
235 }
236
237 // Original should be root again
238 if wm.root != original {
239 t.Error("Original should be promoted to root")
240 }
241 if original.parent != nil {
242 t.Error("Promoted node should have no parent")
243 }
244
245 validateTreeStructure(t, wm.root)
246 })
247
248 t.Run("Close in complex tree", func(t *testing.T) {
249 wm := newTestWorkspaceManagerWithMocksForTree(t)
250
251 // Build tree: root splits right to A and B, then B splits down to C and D
252 A := wm.root
253 B, _ := wm.splitNode(A, "right")
254 C, _ := wm.splitNode(B, "up")
255 // D is the node that B became after split (positioned down)
256 D := B.parent.right
257
258 // Close C - D should be promoted to take the place of C's parent
259 err := wm.closePane(C)
260 if err != nil {
261 t.Fatalf("Close failed: %v", err)
262 }
263
264 validateTreeStructure(t, wm.root)
265
266 // Tree should now have A and D as siblings
267 if wm.root.left != A && wm.root.right != A {
268 t.Error("A should still be child of root")
269 }
270 if wm.root.left != D && wm.root.right != D {
271 t.Error("D should be promoted as child of root")
272 }
273
274 leaves, branches := countNodes(wm.root)
275 if leaves != 2 {
276 t.Errorf("Expected 2 leaves after deletion, got %d", leaves)
277 }
278 if branches != 1 {
279 t.Errorf("Expected 1 branch after deletion, got %d", branches)
280 }
281 })
282}
283
284func TestRootDelegation(t *testing.T) {
285 t.Run("Closing root with siblings delegates correctly", func(t *testing.T) {
286 wm := newTestWorkspaceManagerWithMocksForTree(t)
287
288 // Create structure where root will need delegation
289 originalRoot := wm.root
290 rightNode, _ := wm.splitNode(originalRoot, "right")
291
292 // Now originalRoot and rightNode are siblings under new root
293 // Try to close the left child (originalRoot)
294 err := wm.closePane(originalRoot)
295 if err != nil {
296 t.Fatalf("Close failed: %v", err)
297 }
298
299 // rightNode should now be the root
300 if wm.root != rightNode {
301 t.Error("Right sibling should be promoted to root")
302 }
303 if rightNode.parent != nil {
304 t.Error("New root should have no parent")
305 }
306
307 validateTreeStructure(t, wm.root)
308 })
309}
310
311func TestFindReplacementRoot(t *testing.T) {
312 t.Run("Find replacement in complex tree", func(t *testing.T) {
313 wm := newTestWorkspaceManagerWithMocksForTree(t)
314
315 // Build a complex tree
316 A := wm.root
317 B, _ := wm.splitNode(A, "right")
318 _, _ = wm.splitNode(B, "down")
319
320 // Find replacement excluding A
321 replacement := wm.findReplacementRoot(A)
322
323 if replacement == nil {
324 t.Error("Should find replacement root")
325 }
326 if replacement == A {
327 t.Error("Replacement should not be excluded node")
328 }
329 })
330
331 t.Run("No replacement when only one pane", func(t *testing.T) {
332 wm := newTestWorkspaceManagerWithMocksForTree(t)
333
334 replacement := wm.findReplacementRoot(wm.root)
335 if replacement != nil {
336 t.Error("Should not find replacement when only one pane exists")
337 }
338 })
339}
340
341func TestTreeTraversal(t *testing.T) {
342 t.Run("CollectLeaves gathers all leaf nodes", func(t *testing.T) {
343 wm := newTestWorkspaceManagerWithMocksForTree(t)
344
345 // Create tree with 4 leaves
346 // Keep track of the original root which will become a leaf after split
347 original := wm.root
348 node1, _ := wm.splitNode(original, "right")
349 _, _ = wm.splitNode(node1, "down")
350 _, _ = wm.splitNode(original, "down") // Split the original leaf, not the new root branch
351
352 leaves := wm.collectLeaves()
353
354 if len(leaves) != 4 {
355 t.Errorf("Expected 4 leaves, got %d", len(leaves))
356 }
357
358 // All collected nodes should be leaves
359 for _, leaf := range leaves {
360 if !leaf.isLeaf {
361 t.Error("Non-leaf node in collectLeaves result")
362 }
363 }
364 })
365
366 t.Run("LeftmostLeaf finds correct node", func(t *testing.T) {
367 wm := newTestWorkspaceManagerWithMocksForTree(t)
368
369 original := wm.root
370 right, _ := wm.splitNode(original, "right")
371 _, _ = wm.splitNode(right, "right")
372
373 leftmost := wm.leftmostLeaf(wm.root)
374 if leftmost != original {
375 t.Error("Leftmost should be original node")
376 }
377 })
378
379 t.Run("LeftmostLeaf handles nil", func(t *testing.T) {
380 wm := newTestWorkspaceManagerWithMocksForTree(t)
381
382 leftmost := wm.leftmostLeaf(nil)
383 if leftmost != nil {
384 t.Error("LeftmostLeaf should return nil for nil input")
385 }
386 })
387}
388
389func TestErrorHandling(t *testing.T) {
390 t.Run("Cannot split non-leaf", func(t *testing.T) {
391 wm := newTestWorkspaceManagerWithMocksForTree(t)
392
393 // Create a branch node
394 _, err := wm.splitNode(wm.root, "right")
395 if err != nil {
396 t.Fatalf("Failed to create branch node: %v", err)
397 }
398
399 // Try to split the branch (root is now a branch)
400 _, err = wm.splitNode(wm.root, "down")
401
402 if err == nil {
403 t.Error("Should not allow splitting branch nodes")
404 }
405 })
406
407 t.Run("Cannot close non-leaf", func(t *testing.T) {
408 wm := newTestWorkspaceManagerWithMocksForTree(t)
409
410 _, err := wm.splitNode(wm.root, "right")
411 if err != nil {
412 t.Fatalf("Failed to create branch node: %v", err)
413 }
414
415 // Try to close the branch node
416 err = wm.closePane(wm.root)
417
418 if err == nil {
419 t.Error("Should not allow closing branch nodes")
420 }
421 })
422
423 t.Run("Cannot split nil node", func(t *testing.T) {
424 wm := newTestWorkspaceManagerWithMocksForTree(t)
425
426 _, err := wm.splitNode(nil, "right")
427
428 if err == nil {
429 t.Error("Should not allow splitting nil nodes")
430 }
431 })
432
433 t.Run("Cannot close nil node", func(t *testing.T) {
434 wm := newTestWorkspaceManagerWithMocksForTree(t)
435
436 err := wm.closePane(nil)
437
438 if err == nil {
439 t.Error("Should not allow closing nil nodes")
440 }
441 })
442}
443
444func TestFocusManagement(t *testing.T) {
445 t.Run("Focus transfers after closing active pane", func(t *testing.T) {
446 wm := newTestWorkspaceManagerWithMocksForTree(t)
447
448 original := wm.root
449 newNode, _ := wm.splitNode(original, "right")
450
451 // Set new node as active
452 wm.active = newNode
453
454 // Close the active node
455 err := wm.closePane(newNode)
456 if err != nil {
457 t.Fatalf("Close failed: %v", err)
458 }
459
460 // Focus should transfer to original
461 if wm.active != original {
462 t.Error("Focus should transfer to remaining pane")
463 }
464 })
465
466 t.Run("Focus preserved when closing non-active pane", func(t *testing.T) {
467 wm := newTestWorkspaceManagerWithMocksForTree(t)
468
469 original := wm.root
470 newNode, _ := wm.splitNode(original, "right")
471
472 // Keep original as active
473 wm.active = original
474
475 // Close the non-active node
476 err := wm.closePane(newNode)
477 if err != nil {
478 t.Fatalf("Close failed: %v", err)
479 }
480
481 // Focus should remain on original
482 if wm.active != original {
483 t.Error("Focus should remain on original pane")
484 }
485 })
486}
487
488// TestBugClosePaneWithBranchSibling reproduces the bug where closing a pane
489// whose sibling is a branch (not a leaf) causes "unexpected tree state" error
490func TestBugClosePaneWithBranchSibling(t *testing.T) {
491 t.Run("Closing pane when sibling is branch with children", func(t *testing.T) {
492 wm := newTestWorkspaceManagerWithMocksForTree(t)
493
494 // Create the exact scenario from the bug:
495 // 1. Start with root pane A
496 A := wm.root
497
498 // 2. Split down to create B (A is now top, B is bottom)
499 B, err := wm.splitNode(A, "down")
500 if err != nil {
501 t.Fatalf("Split down failed: %v", err)
502 }
503
504 // After this split:
505 // Root (vertical paned) has A (top) and B (bottom)
506
507 // 3. Split B down again to create C
508 C, err := wm.splitNode(B, "down")
509 if err != nil {
510 t.Fatalf("Split B down failed: %v", err)
511 }
512
513 // After this split, tree structure is:
514 // Root (vertical) -> A (top), SubPaned (vertical, bottom)
515 // SubPaned -> B (top), C (bottom)
516 // Total: 3 leaf panes (A, B, C), 2 branch nodes
517
518 // Update app.panes to reflect actual structure
519 wm.app.panes = []*BrowserPane{A.pane, B.pane, C.pane}
520
521 // Verify initial structure
522 validateTreeStructure(t, wm.root)
523 leaves, branches := countNodes(wm.root)
524 if leaves != 3 {
525 t.Errorf("Expected 3 leaves initially, got %d", leaves)
526 }
527 if branches != 2 {
528 t.Errorf("Expected 2 branches initially, got %d", branches)
529 }
530
531 // Now close pane A - this should promote the SubPaned (containing B and C) to root
532 // This is where the bug occurs: parent is root, but 2 panes remain (B and C)
533 // The current code incorrectly thinks this is an error
534 err = wm.closePane(A)
535
536 // THIS IS THE BUG: The current implementation returns an error here
537 // but it shouldn't - it's a valid operation
538 if err != nil {
539 // For now, we expect this to fail with the bug
540 if err.Error() == "unexpected tree state during pane closure" {
541 t.Logf("Bug reproduced: %v", err)
542 t.Logf("This error should not occur - closing pane with branch sibling is valid")
543 // Comment out the return to see what the final state should be
544 // return
545 } else {
546 t.Fatalf("Unexpected error: %v", err)
547 }
548 }
549
550 // What SHOULD happen after successful close:
551 // The SubPaned (containing B and C) should become the new root
552 validateTreeStructure(t, wm.root)
553
554 // Verify final state (what we expect after fix)
555 finalLeaves, finalBranches := countNodes(wm.root)
556 if finalLeaves != 2 {
557 t.Errorf("Expected 2 leaves after close, got %d", finalLeaves)
558 }
559 if finalBranches != 1 {
560 t.Errorf("Expected 1 branch after close, got %d", finalBranches)
561 }
562
563 // The new root should be a branch (the old SubPaned)
564 if wm.root.isLeaf {
565 t.Error("Root should be a branch node after closing A")
566 }
567
568 // B and C should still be in the tree
569 foundB := false
570 foundC := false
571 for _, leaf := range wm.collectLeaves() {
572 if leaf == B {
573 foundB = true
574 }
575 if leaf == C {
576 foundC = true
577 }
578 }
579 if !foundB {
580 t.Error("Pane B should still be in tree")
581 }
582 if !foundC {
583 t.Error("Pane C should still be in tree")
584 }
585 })
586
587 t.Run("Alternative scenario - closing left child when right has grandchildren", func(t *testing.T) {
588 wm := newTestWorkspaceManagerWithMocksForTree(t)
589
590 // Create structure: A | (B / C)
591 // where | is horizontal split and / is vertical split
592 A := wm.root
593 B, err := wm.splitNode(A, "right")
594 if err != nil {
595 t.Fatalf("Split right failed: %v", err)
596 }
597
598 C, err := wm.splitNode(B, "down")
599 if err != nil {
600 t.Fatalf("Split down failed: %v", err)
601 }
602
603 // Update app.panes
604 wm.app.panes = []*BrowserPane{A.pane, B.pane, C.pane}
605
606 // Tree is: Root(h) -> A, Paned(v) -> B, C
607 validateTreeStructure(t, wm.root)
608
609 // Close A - the Paned(v) containing B and C should become root
610 err = wm.closePane(A)
611
612 // Bug should occur here too
613 if err != nil {
614 if err.Error() == "unexpected tree state during pane closure" {
615 t.Logf("Bug reproduced in alternative scenario: %v", err)
616 }
617 }
618
619 // After fix, the vertical paned with B and C should be root
620 validateTreeStructure(t, wm.root)
621 })
622}
623
624func TestEdgeCases(t *testing.T) {
625 t.Run("Closing last pane", func(t *testing.T) {
626 wm := newTestWorkspaceManagerWithMocksForTree(t)
627
628 // Mock the app.panes slice to have only one pane
629 wm.app.panes = []*BrowserPane{wm.root.pane}
630
631 // Closing the last pane should handle gracefully
632 err := wm.closePane(wm.root)
633 if err != nil {
634 t.Logf("Expected behavior: closing last pane returned error: %v", err)
635 }
636 })
637
638 t.Run("Split with invalid direction", func(t *testing.T) {
639 wm := newTestWorkspaceManagerWithMocksForTree(t)
640
641 // Test default behavior with invalid direction
642 newNode, err := wm.splitNode(wm.root, "invalid")
643 if err != nil {
644 t.Fatalf("Split with invalid direction failed: %v", err)
645 }
646
647 // Should default to horizontal orientation
648 if newNode.parent.orientation != webkit.OrientationHorizontal {
649 t.Error("Invalid direction should default to horizontal")
650 }
651 })
652}
653
654func TestInfiniteSiblingsClosingIssue(t *testing.T) {
655 t.Run("Infinite siblings issue reproduction", func(t *testing.T) {
656 wm := newTestWorkspaceManagerWithMocksForTree(t)
657
658 // Reproduce the scenario: Create multiple siblings by repeatedly splitting right
659 original := wm.root
660 rightPane1, err := wm.splitNode(original, "right")
661 if err != nil {
662 t.Fatalf("First split failed: %v", err)
663 }
664
665 // Split the right pane down to create a nested structure
666 downPane, err := wm.splitNode(rightPane1, "down")
667 if err != nil {
668 t.Fatalf("Down split failed: %v", err)
669 }
670
671 // Verify tree structure is correct
672 validateTreeStructure(t, wm.root)
673
674 // Count initial panes
675 initialLeaves, initialBranches := countNodes(wm.root)
676 if initialLeaves != 3 {
677 t.Errorf("Expected 3 leaves after splits, got %d", initialLeaves)
678 }
679 if initialBranches != 2 {
680 t.Errorf("Expected 2 branches after splits, got %d", initialBranches)
681 }
682
683 // Update app.panes to reflect the actual structure
684 wm.app.panes = []*BrowserPane{original.pane, rightPane1.pane, downPane.pane}
685
686 // First close the downPane (the non-direct sibling)
687 err = wm.closePane(downPane)
688 if err != nil {
689 t.Fatalf("Close downPane failed: %v", err)
690 }
691
692 validateTreeStructure(t, wm.root)
693
694 // Now try to close rightPane1 (which should leave only original)
695 // This should cause the promotion of original to root
696 err = wm.closePane(rightPane1)
697 if err != nil {
698 t.Fatalf("Close rightPane1 failed: %v", err)
699 }
700
701 validateTreeStructure(t, wm.root)
702
703 // Final tree should have only original as root
704 if wm.root != original {
705 t.Error("Original should be promoted to root after closing siblings")
706 }
707 if !wm.root.isLeaf {
708 t.Error("Root should be a leaf after closing all siblings")
709 }
710
711 finalLeaves, finalBranches := countNodes(wm.root)
712 if finalLeaves != 1 {
713 t.Errorf("Expected 1 leaf after closing siblings, got %d", finalLeaves)
714 }
715 if finalBranches != 0 {
716 t.Errorf("Expected 0 branches after closing siblings, got %d", finalBranches)
717 }
718 })
719
720 t.Run("Closing direct sibling of root reproduces GTK error", func(t *testing.T) {
721 wm := newTestWorkspaceManagerWithMocksForTree(t)
722
723 // Create a simple two-pane layout (root + sibling)
724 original := wm.root
725 sibling, err := wm.splitNode(original, "right")
726 if err != nil {
727 t.Fatalf("Split failed: %v", err)
728 }
729
730 // Update app.panes
731 wm.app.panes = []*BrowserPane{original.pane, sibling.pane}
732
733 validateTreeStructure(t, wm.root)
734
735 // Now close the sibling - this should promote original back to root
736 // and should NOT cause GTK-CRITICAL errors
737 err = wm.closePane(sibling)
738 if err != nil {
739 t.Fatalf("Close sibling failed: %v", err)
740 }
741
742 validateTreeStructure(t, wm.root)
743
744 // Verify the tree is properly cleaned up
745 if wm.root != original {
746 t.Error("Original should be promoted to root")
747 }
748 if !wm.root.isLeaf {
749 t.Error("Root should be a leaf after closing sibling")
750 }
751 if wm.root.parent != nil {
752 t.Error("Root should have no parent")
753 }
754 })
755
756 t.Run("Complex closing sequence without direct root sibling", func(t *testing.T) {
757 wm := newTestWorkspaceManagerWithMocksForTree(t)
758
759 // Create a more complex tree that can test the issue where
760 // closing requires closing until only the direct sibling of root remains
761
762 // Start with root (A)
763 A := wm.root
764
765 // Split right to create B
766 B, err := wm.splitNode(A, "right")
767 if err != nil {
768 t.Fatalf("Split A->B failed: %v", err)
769 }
770
771 // Split B down to create C (B becomes internal node, C is new leaf)
772 C, err := wm.splitNode(B, "down")
773 if err != nil {
774 t.Fatalf("Split B->C failed: %v", err)
775 }
776 // After this split, the tree structure is:
777 // Root(horizontal) -> A (left), Parent(vertical) (right)
778 // -> B (top), C (bottom)
779
780 // Split C right to create D
781 D, err := wm.splitNode(C, "right")
782 if err != nil {
783 t.Fatalf("Split C->D failed: %v", err)
784 }
785
786 // Update app.panes to reflect actual panes
787 wm.app.panes = []*BrowserPane{A.pane, B.pane, C.pane, D.pane}
788
789 validateTreeStructure(t, wm.root)
790
791 // Count nodes to verify structure
792 leaves, branches := countNodes(wm.root)
793 if leaves != 4 {
794 t.Errorf("Expected 4 leaves, got %d", leaves)
795 }
796 if branches != 3 {
797 t.Errorf("Expected 3 branches, got %d", branches)
798 }
799
800 // Now start closing panes in a sequence that would eventually
801 // require promoting siblings and potentially trigger the GTK error
802
803 // Close D first
804 err = wm.closePane(D)
805 if err != nil {
806 t.Fatalf("Close D failed: %v", err)
807 }
808 validateTreeStructure(t, wm.root)
809
810 // Close C next
811 err = wm.closePane(C)
812 if err != nil {
813 t.Fatalf("Close C failed: %v", err)
814 }
815 validateTreeStructure(t, wm.root)
816
817 // Close B next - this should leave only A and trigger the root sibling issue
818 err = wm.closePane(B)
819 if err != nil {
820 t.Fatalf("Close B failed: %v", err)
821 }
822 validateTreeStructure(t, wm.root)
823
824 // Verify final state
825 if wm.root != A {
826 t.Error("A should be the final root")
827 }
828 if !wm.root.isLeaf {
829 t.Error("Final root should be a leaf")
830 }
831
832 finalLeaves, finalBranches := countNodes(wm.root)
833 if finalLeaves != 1 {
834 t.Errorf("Expected 1 final leaf, got %d", finalLeaves)
835 }
836 if finalBranches != 0 {
837 t.Errorf("Expected 0 final branches, got %d", finalBranches)
838 }
839 })
840}
841
842func TestViewToNodeMapping(t *testing.T) {
843 t.Run("ViewToNode map updates correctly", func(t *testing.T) {
844 wm := newTestWorkspaceManagerWithMocksForTree(t)
845
846 originalNode := wm.root
847 originalView := wm.root.pane.webView
848
849 // Split should add new mapping
850 newNode, _ := wm.splitNode(wm.root, "right")
851 newView := newNode.pane.webView
852
853 // Check mapping exists for new view
854 if mapped, ok := wm.viewToNode[newView]; !ok || mapped != newNode {
855 t.Error("New webview should be mapped to new node")
856 }
857
858 // Original mapping should still exist - note that wm.root may have changed after split
859 if mapped, ok := wm.viewToNode[originalView]; !ok || mapped != originalNode {
860 t.Error("Original webview mapping should be preserved")
861 }
862
863 // Close should remove mapping
864 err := wm.closePane(newNode)
865 if err != nil {
866 t.Fatalf("Failed to close pane: %v", err)
867 }
868
869 if _, ok := wm.viewToNode[newView]; ok {
870 t.Error("Closed pane's webview should be removed from mapping")
871 }
872 })
873}
874
875// Helper to create WorkspaceManager with minimal mocked dependencies
876func newTestWorkspaceManagerWithMocksForTree(t *testing.T) *WorkspaceManager {
877 t.Helper()
878 webkit.ResetWidgetStubsForTesting()
879
880 // Create a real WebView for testing (stub implementation)
881 rootView, err := webkit.NewWebView(&webkit.Config{})
882 if err != nil {
883 t.Fatalf("failed to create root webview: %v", err)
884 }
885
886 rootPane := &BrowserPane{
887 webView: rootView,
888 id: "root",
889 }
890
891 // Create mock window shortcut handler
892 mockWindowShortcutHandler := &mockWindowShortcutHandler{}
893
894 app := &BrowserApp{
895 panes: []*BrowserPane{rootPane},
896 activePane: rootPane,
897 webView: rootView,
898 windowShortcutHandler: mockWindowShortcutHandler,
899 }
900
901 wm := &WorkspaceManager{
902 app: app,
903 window: rootView.Window(),
904 viewToNode: make(map[*webkit.WebView]*paneNode),
905 lastSplitMsg: make(map[*webkit.WebView]time.Time),
906 lastExitMsg: make(map[*webkit.WebView]time.Time),
907 }
908
909 // Set up mock factories
910 wm.createWebViewFn = func() (*webkit.WebView, error) {
911 return webkit.NewWebView(&webkit.Config{})
912 }
913
914 wm.createPaneFn = func(view *webkit.WebView) (*BrowserPane, error) {
915 return &BrowserPane{
916 webView: view,
917 id: fmt.Sprintf("pane-%p", view),
918 }, nil
919 }
920
921 // Initialize root
922 root := &paneNode{
923 pane: rootPane,
924 container: rootView.RootWidget(),
925 isLeaf: true,
926 }
927 wm.root = root
928 wm.active = root
929 wm.mainPane = root
930 wm.viewToNode[rootView] = root
931
932 return wm
933}