chore(browser): remove unit test files
- commit
- 333d85c7bfa1417277774dacf91436dfbc04257f
- author
- Brice · · 2025-09-27 19:28 UTC
- parent
- 963bbe1
- signature
- unsigned
6 files · +0 -3941
D internal/app/browser/test_helpers.go +0 -8
| @@ -1,8 +0,0 @@ | |||
| 1 | - | package browser | |
| 2 | - | ||
| 3 | - | // Mock WindowShortcutHandler for testing | |
| 4 | - | type mockWindowShortcutHandler struct{} | |
| 5 | - | ||
| 6 | - | func (m *mockWindowShortcutHandler) Cleanup() { | |
| 7 | - | // No-op for testing | |
| 8 | - | } | |
| @@ -1,8 +0,0 @@ | |||||
| 1 | - | package browser | |||
| 2 | - | ||||
| 3 | - | // Mock WindowShortcutHandler for testing | |||
| 4 | - | type mockWindowShortcutHandler struct{} | |||
| 5 | - | ||||
| 6 | - | func (m *mockWindowShortcutHandler) Cleanup() { | |||
| 7 | - | // No-op for testing | |||
| 8 | - | } | |||
D internal/app/browser/workspace_manager_bench_test.go +0 -344
| @@ -1,344 +0,0 @@ | |||
| 1 | - | package browser | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | "time" | |
| 6 | - | ||
| 7 | - | "github.com/bnema/dumber/pkg/webkit" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | func BenchmarkSplitNode(b *testing.B) { | |
| 11 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 12 | - | ||
| 13 | - | b.ResetTimer() | |
| 14 | - | for i := 0; i < b.N; i++ { | |
| 15 | - | b.StopTimer() | |
| 16 | - | // Reset to single node for each iteration | |
| 17 | - | wm = newTestWorkspaceManagerWithMocks(b) | |
| 18 | - | b.StartTimer() | |
| 19 | - | ||
| 20 | - | _, err := wm.splitNode(wm.root, "right") | |
| 21 | - | if err != nil { | |
| 22 | - | b.Fatalf("Split failed: %v", err) | |
| 23 | - | } | |
| 24 | - | } | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | func BenchmarkClosePane(b *testing.B) { | |
| 28 | - | for i := 0; i < b.N; i++ { | |
| 29 | - | b.StopTimer() | |
| 30 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 31 | - | node, err := wm.splitNode(wm.root, "right") | |
| 32 | - | if err != nil { | |
| 33 | - | b.Fatalf("Setup split failed: %v", err) | |
| 34 | - | } | |
| 35 | - | b.StartTimer() | |
| 36 | - | ||
| 37 | - | err = wm.closePane(node) | |
| 38 | - | if err != nil { | |
| 39 | - | b.Fatalf("Close failed: %v", err) | |
| 40 | - | } | |
| 41 | - | } | |
| 42 | - | } | |
| 43 | - | ||
| 44 | - | func BenchmarkTreeTraversal(b *testing.B) { | |
| 45 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 46 | - | ||
| 47 | - | // Build a complex tree with 15 leaves | |
| 48 | - | leaves := []*paneNode{wm.root} | |
| 49 | - | for i := 0; i < 14; i++ { | |
| 50 | - | if len(leaves) > 0 { | |
| 51 | - | // Split the first leaf | |
| 52 | - | target := leaves[0] | |
| 53 | - | leaves = leaves[1:] // Remove the target from leaves | |
| 54 | - | ||
| 55 | - | newNode, err := wm.splitNode(target, "right") | |
| 56 | - | if err != nil { | |
| 57 | - | b.Fatalf("Setup split %d failed: %v", i, err) | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | // Add both resulting leaves back | |
| 61 | - | leaves = append(leaves, target, newNode) | |
| 62 | - | } | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | b.ResetTimer() | |
| 66 | - | for i := 0; i < b.N; i++ { | |
| 67 | - | wm.collectLeaves() | |
| 68 | - | } | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | func BenchmarkCollectLeavesFrom(b *testing.B) { | |
| 72 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 73 | - | ||
| 74 | - | // Build a balanced binary tree | |
| 75 | - | for i := 0; i < 10; i++ { | |
| 76 | - | leaves := wm.collectLeaves() | |
| 77 | - | if len(leaves) > 0 { | |
| 78 | - | _, err := wm.splitNode(leaves[0], "right") | |
| 79 | - | if err != nil { | |
| 80 | - | b.Fatalf("Setup split %d failed: %v", i, err) | |
| 81 | - | } | |
| 82 | - | } | |
| 83 | - | } | |
| 84 | - | ||
| 85 | - | b.ResetTimer() | |
| 86 | - | for i := 0; i < b.N; i++ { | |
| 87 | - | wm.collectLeavesFrom(wm.root) | |
| 88 | - | } | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | func BenchmarkLeftmostLeaf(b *testing.B) { | |
| 92 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 93 | - | ||
| 94 | - | // Create a deep left-leaning tree | |
| 95 | - | current := wm.root | |
| 96 | - | for i := 0; i < 20; i++ { | |
| 97 | - | newNode, err := wm.splitNode(current, "left") | |
| 98 | - | if err != nil { | |
| 99 | - | b.Fatalf("Setup split %d failed: %v", i, err) | |
| 100 | - | } | |
| 101 | - | current = newNode | |
| 102 | - | } | |
| 103 | - | ||
| 104 | - | b.ResetTimer() | |
| 105 | - | for i := 0; i < b.N; i++ { | |
| 106 | - | wm.leftmostLeaf(wm.root) | |
| 107 | - | } | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | func BenchmarkFindReplacementRoot(b *testing.B) { | |
| 111 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 112 | - | ||
| 113 | - | // Build a complex tree | |
| 114 | - | leaves := wm.collectLeaves() | |
| 115 | - | for i := 0; i < 15; i++ { | |
| 116 | - | if len(leaves) > 0 { | |
| 117 | - | target := leaves[i%len(leaves)] | |
| 118 | - | newNode, err := wm.splitNode(target, "right") | |
| 119 | - | if err != nil { | |
| 120 | - | continue | |
| 121 | - | } | |
| 122 | - | leaves = append(leaves, newNode) | |
| 123 | - | } | |
| 124 | - | } | |
| 125 | - | ||
| 126 | - | excludeNode := wm.root | |
| 127 | - | ||
| 128 | - | b.ResetTimer() | |
| 129 | - | for i := 0; i < b.N; i++ { | |
| 130 | - | wm.findReplacementRoot(excludeNode) | |
| 131 | - | } | |
| 132 | - | } | |
| 133 | - | ||
| 134 | - | func BenchmarkFocusAdjacent(b *testing.B) { | |
| 135 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 136 | - | ||
| 137 | - | // Create a 2x2 grid of panes with known geometry | |
| 138 | - | right, _ := wm.splitNode(wm.root, "right") | |
| 139 | - | bottom, _ := wm.splitNode(wm.root, "down") | |
| 140 | - | _, _ = wm.splitNode(right, "down") | |
| 141 | - | ||
| 142 | - | // Set up widget bounds for focus calculations | |
| 143 | - | webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |
| 144 | - | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |
| 145 | - | webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 100}) | |
| 146 | - | ||
| 147 | - | wm.currentlyFocused = wm.root | |
| 148 | - | ||
| 149 | - | b.ResetTimer() | |
| 150 | - | for i := 0; i < b.N; i++ { | |
| 151 | - | wm.focusAdjacent("right") | |
| 152 | - | wm.focusAdjacent("left") | |
| 153 | - | } | |
| 154 | - | } | |
| 155 | - | ||
| 156 | - | func BenchmarkStructuralNeighbor(b *testing.B) { | |
| 157 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 158 | - | ||
| 159 | - | // Create a complex nested structure | |
| 160 | - | right, _ := wm.splitNode(wm.root, "right") | |
| 161 | - | _, _ = wm.splitNode(right, "down") | |
| 162 | - | _, _ = wm.splitNode(wm.root, "down") | |
| 163 | - | ||
| 164 | - | // Set up geometry | |
| 165 | - | webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |
| 166 | - | ||
| 167 | - | b.ResetTimer() | |
| 168 | - | for i := 0; i < b.N; i++ { | |
| 169 | - | wm.structuralNeighbor(wm.root, "right") | |
| 170 | - | wm.structuralNeighbor(wm.root, "down") | |
| 171 | - | } | |
| 172 | - | } | |
| 173 | - | ||
| 174 | - | func BenchmarkClosestLeafFromSubtree(b *testing.B) { | |
| 175 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 176 | - | ||
| 177 | - | // Build a subtree with multiple leaves | |
| 178 | - | right, _ := wm.splitNode(wm.root, "right") | |
| 179 | - | _, _ = wm.splitNode(right, "down") | |
| 180 | - | _, _ = wm.splitNode(right, "up") | |
| 181 | - | ||
| 182 | - | // Set up geometry for all nodes | |
| 183 | - | leaves := wm.collectLeavesFrom(right) | |
| 184 | - | for i, leaf := range leaves { | |
| 185 | - | webkit.SetWidgetBoundsForTesting(leaf.container, webkit.WidgetBounds{ | |
| 186 | - | X: float64(i * 50), Y: 0, Width: 45, Height: 100, | |
| 187 | - | }) | |
| 188 | - | } | |
| 189 | - | ||
| 190 | - | b.ResetTimer() | |
| 191 | - | for i := 0; i < b.N; i++ { | |
| 192 | - | wm.closestLeafFromSubtree(right, 25, 50, "right") | |
| 193 | - | } | |
| 194 | - | } | |
| 195 | - | ||
| 196 | - | func BenchmarkComplexTreeOperations(b *testing.B) { | |
| 197 | - | b.Run("SplitAndClose", func(b *testing.B) { | |
| 198 | - | for i := 0; i < b.N; i++ { | |
| 199 | - | b.StopTimer() | |
| 200 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 201 | - | b.StartTimer() | |
| 202 | - | ||
| 203 | - | // Split multiple times then close in reverse order | |
| 204 | - | nodes := []*paneNode{wm.root} | |
| 205 | - | for j := 0; j < 5; j++ { | |
| 206 | - | newNode, err := wm.splitNode(nodes[len(nodes)-1], "right") | |
| 207 | - | if err != nil { | |
| 208 | - | b.Fatalf("Split failed: %v", err) | |
| 209 | - | } | |
| 210 | - | nodes = append(nodes, newNode) | |
| 211 | - | } | |
| 212 | - | ||
| 213 | - | // Close all but root | |
| 214 | - | for j := len(nodes) - 1; j > 0; j-- { | |
| 215 | - | err := wm.closePane(nodes[j]) | |
| 216 | - | if err != nil { | |
| 217 | - | b.Fatalf("Close failed: %v", err) | |
| 218 | - | } | |
| 219 | - | } | |
| 220 | - | } | |
| 221 | - | }) | |
| 222 | - | ||
| 223 | - | b.Run("BuildBalancedTree", func(b *testing.B) { | |
| 224 | - | for i := 0; i < b.N; i++ { | |
| 225 | - | b.StopTimer() | |
| 226 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 227 | - | b.StartTimer() | |
| 228 | - | ||
| 229 | - | // Build a balanced tree by alternating split directions | |
| 230 | - | leaves := []*paneNode{wm.root} | |
| 231 | - | directions := []string{"right", "down", "left", "up"} | |
| 232 | - | ||
| 233 | - | for j := 0; j < 15 && len(leaves) > 0; j++ { | |
| 234 | - | target := leaves[j%len(leaves)] | |
| 235 | - | direction := directions[j%len(directions)] | |
| 236 | - | ||
| 237 | - | newNode, err := wm.splitNode(target, direction) | |
| 238 | - | if err != nil { | |
| 239 | - | continue | |
| 240 | - | } | |
| 241 | - | leaves = append(leaves, newNode) | |
| 242 | - | } | |
| 243 | - | } | |
| 244 | - | }) | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | func BenchmarkMemoryOperations(b *testing.B) { | |
| 248 | - | b.Run("ViewToNodeMapping", func(b *testing.B) { | |
| 249 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 250 | - | ||
| 251 | - | // Create many nodes to stress the map | |
| 252 | - | for i := 0; i < 100; i++ { | |
| 253 | - | leaves := wm.collectLeaves() | |
| 254 | - | if len(leaves) > 0 { | |
| 255 | - | _, _ = wm.splitNode(leaves[0], "right") | |
| 256 | - | } | |
| 257 | - | } | |
| 258 | - | ||
| 259 | - | b.ResetTimer() | |
| 260 | - | for i := 0; i < b.N; i++ { | |
| 261 | - | // Access all mappings | |
| 262 | - | for view, node := range wm.viewToNode { | |
| 263 | - | _ = view | |
| 264 | - | _ = node | |
| 265 | - | } | |
| 266 | - | } | |
| 267 | - | }) | |
| 268 | - | ||
| 269 | - | b.Run("TreeWalkMemory", func(b *testing.B) { | |
| 270 | - | wm := newTestWorkspaceManagerWithMocks(b) | |
| 271 | - | ||
| 272 | - | // Build deep tree | |
| 273 | - | current := wm.root | |
| 274 | - | for i := 0; i < 50; i++ { | |
| 275 | - | newNode, err := wm.splitNode(current, "right") | |
| 276 | - | if err != nil { | |
| 277 | - | break | |
| 278 | - | } | |
| 279 | - | current = newNode | |
| 280 | - | } | |
| 281 | - | ||
| 282 | - | b.ResetTimer() | |
| 283 | - | for i := 0; i < b.N; i++ { | |
| 284 | - | // Multiple tree operations that allocate | |
| 285 | - | leaves := wm.collectLeaves() | |
| 286 | - | _ = leaves | |
| 287 | - | countNodes(wm.root) | |
| 288 | - | wm.leftmostLeaf(wm.root) | |
| 289 | - | } | |
| 290 | - | }) | |
| 291 | - | } | |
| 292 | - | ||
| 293 | - | // Helper function for benchmarks | |
| 294 | - | func newTestWorkspaceManagerWithMocks(tb testing.TB) *WorkspaceManager { | |
| 295 | - | tb.Helper() | |
| 296 | - | webkit.ResetWidgetStubsForTesting() | |
| 297 | - | ||
| 298 | - | rootView, err := webkit.NewWebView(&webkit.Config{}) | |
| 299 | - | if err != nil { | |
| 300 | - | tb.Fatalf("failed to create root webview: %v", err) | |
| 301 | - | } | |
| 302 | - | ||
| 303 | - | rootPane := &BrowserPane{ | |
| 304 | - | webView: rootView, | |
| 305 | - | id: "root", | |
| 306 | - | } | |
| 307 | - | ||
| 308 | - | app := &BrowserApp{ | |
| 309 | - | panes: []*BrowserPane{rootPane}, | |
| 310 | - | activePane: rootPane, | |
| 311 | - | webView: rootView, | |
| 312 | - | } | |
| 313 | - | ||
| 314 | - | wm := &WorkspaceManager{ | |
| 315 | - | app: app, | |
| 316 | - | window: rootView.Window(), | |
| 317 | - | viewToNode: make(map[*webkit.WebView]*paneNode), | |
| 318 | - | lastSplitMsg: make(map[*webkit.WebView]time.Time), | |
| 319 | - | lastExitMsg: make(map[*webkit.WebView]time.Time), | |
| 320 | - | } | |
| 321 | - | ||
| 322 | - | wm.createWebViewFn = func() (*webkit.WebView, error) { | |
| 323 | - | return webkit.NewWebView(&webkit.Config{}) | |
| 324 | - | } | |
| 325 | - | ||
| 326 | - | wm.createPaneFn = func(view *webkit.WebView) (*BrowserPane, error) { | |
| 327 | - | return &BrowserPane{ | |
| 328 | - | webView: view, | |
| 329 | - | id: "bench-pane", | |
| 330 | - | }, nil | |
| 331 | - | } | |
| 332 | - | ||
| 333 | - | root := &paneNode{ | |
| 334 | - | pane: rootPane, | |
| 335 | - | container: rootView.RootWidget(), | |
| 336 | - | isLeaf: true, | |
| 337 | - | } | |
| 338 | - | wm.root = root | |
| 339 | - | wm.currentlyFocused = root | |
| 340 | - | wm.mainPane = root | |
| 341 | - | wm.viewToNode[rootView] = root | |
| 342 | - | ||
| 343 | - | return wm | |
| 344 | - | } | |
| @@ -1,344 +0,0 @@ | |||||
| 1 | - | package browser | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "testing" | |||
| 5 | - | "time" | |||
| 6 | - | ||||
| 7 | - | "github.com/bnema/dumber/pkg/webkit" | |||
| 8 | - | ) | |||
| 9 | - | ||||
| 10 | - | func BenchmarkSplitNode(b *testing.B) { | |||
| 11 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 12 | - | ||||
| 13 | - | b.ResetTimer() | |||
| 14 | - | for i := 0; i < b.N; i++ { | |||
| 15 | - | b.StopTimer() | |||
| 16 | - | // Reset to single node for each iteration | |||
| 17 | - | wm = newTestWorkspaceManagerWithMocks(b) | |||
| 18 | - | b.StartTimer() | |||
| 19 | - | ||||
| 20 | - | _, err := wm.splitNode(wm.root, "right") | |||
| 21 | - | if err != nil { | |||
| 22 | - | b.Fatalf("Split failed: %v", err) | |||
| 23 | - | } | |||
| 24 | - | } | |||
| 25 | - | } | |||
| 26 | - | ||||
| 27 | - | func BenchmarkClosePane(b *testing.B) { | |||
| 28 | - | for i := 0; i < b.N; i++ { | |||
| 29 | - | b.StopTimer() | |||
| 30 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 31 | - | node, err := wm.splitNode(wm.root, "right") | |||
| 32 | - | if err != nil { | |||
| 33 | - | b.Fatalf("Setup split failed: %v", err) | |||
| 34 | - | } | |||
| 35 | - | b.StartTimer() | |||
| 36 | - | ||||
| 37 | - | err = wm.closePane(node) | |||
| 38 | - | if err != nil { | |||
| 39 | - | b.Fatalf("Close failed: %v", err) | |||
| 40 | - | } | |||
| 41 | - | } | |||
| 42 | - | } | |||
| 43 | - | ||||
| 44 | - | func BenchmarkTreeTraversal(b *testing.B) { | |||
| 45 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 46 | - | ||||
| 47 | - | // Build a complex tree with 15 leaves | |||
| 48 | - | leaves := []*paneNode{wm.root} | |||
| 49 | - | for i := 0; i < 14; i++ { | |||
| 50 | - | if len(leaves) > 0 { | |||
| 51 | - | // Split the first leaf | |||
| 52 | - | target := leaves[0] | |||
| 53 | - | leaves = leaves[1:] // Remove the target from leaves | |||
| 54 | - | ||||
| 55 | - | newNode, err := wm.splitNode(target, "right") | |||
| 56 | - | if err != nil { | |||
| 57 | - | b.Fatalf("Setup split %d failed: %v", i, err) | |||
| 58 | - | } | |||
| 59 | - | ||||
| 60 | - | // Add both resulting leaves back | |||
| 61 | - | leaves = append(leaves, target, newNode) | |||
| 62 | - | } | |||
| 63 | - | } | |||
| 64 | - | ||||
| 65 | - | b.ResetTimer() | |||
| 66 | - | for i := 0; i < b.N; i++ { | |||
| 67 | - | wm.collectLeaves() | |||
| 68 | - | } | |||
| 69 | - | } | |||
| 70 | - | ||||
| 71 | - | func BenchmarkCollectLeavesFrom(b *testing.B) { | |||
| 72 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 73 | - | ||||
| 74 | - | // Build a balanced binary tree | |||
| 75 | - | for i := 0; i < 10; i++ { | |||
| 76 | - | leaves := wm.collectLeaves() | |||
| 77 | - | if len(leaves) > 0 { | |||
| 78 | - | _, err := wm.splitNode(leaves[0], "right") | |||
| 79 | - | if err != nil { | |||
| 80 | - | b.Fatalf("Setup split %d failed: %v", i, err) | |||
| 81 | - | } | |||
| 82 | - | } | |||
| 83 | - | } | |||
| 84 | - | ||||
| 85 | - | b.ResetTimer() | |||
| 86 | - | for i := 0; i < b.N; i++ { | |||
| 87 | - | wm.collectLeavesFrom(wm.root) | |||
| 88 | - | } | |||
| 89 | - | } | |||
| 90 | - | ||||
| 91 | - | func BenchmarkLeftmostLeaf(b *testing.B) { | |||
| 92 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 93 | - | ||||
| 94 | - | // Create a deep left-leaning tree | |||
| 95 | - | current := wm.root | |||
| 96 | - | for i := 0; i < 20; i++ { | |||
| 97 | - | newNode, err := wm.splitNode(current, "left") | |||
| 98 | - | if err != nil { | |||
| 99 | - | b.Fatalf("Setup split %d failed: %v", i, err) | |||
| 100 | - | } | |||
| 101 | - | current = newNode | |||
| 102 | - | } | |||
| 103 | - | ||||
| 104 | - | b.ResetTimer() | |||
| 105 | - | for i := 0; i < b.N; i++ { | |||
| 106 | - | wm.leftmostLeaf(wm.root) | |||
| 107 | - | } | |||
| 108 | - | } | |||
| 109 | - | ||||
| 110 | - | func BenchmarkFindReplacementRoot(b *testing.B) { | |||
| 111 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 112 | - | ||||
| 113 | - | // Build a complex tree | |||
| 114 | - | leaves := wm.collectLeaves() | |||
| 115 | - | for i := 0; i < 15; i++ { | |||
| 116 | - | if len(leaves) > 0 { | |||
| 117 | - | target := leaves[i%len(leaves)] | |||
| 118 | - | newNode, err := wm.splitNode(target, "right") | |||
| 119 | - | if err != nil { | |||
| 120 | - | continue | |||
| 121 | - | } | |||
| 122 | - | leaves = append(leaves, newNode) | |||
| 123 | - | } | |||
| 124 | - | } | |||
| 125 | - | ||||
| 126 | - | excludeNode := wm.root | |||
| 127 | - | ||||
| 128 | - | b.ResetTimer() | |||
| 129 | - | for i := 0; i < b.N; i++ { | |||
| 130 | - | wm.findReplacementRoot(excludeNode) | |||
| 131 | - | } | |||
| 132 | - | } | |||
| 133 | - | ||||
| 134 | - | func BenchmarkFocusAdjacent(b *testing.B) { | |||
| 135 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 136 | - | ||||
| 137 | - | // Create a 2x2 grid of panes with known geometry | |||
| 138 | - | right, _ := wm.splitNode(wm.root, "right") | |||
| 139 | - | bottom, _ := wm.splitNode(wm.root, "down") | |||
| 140 | - | _, _ = wm.splitNode(right, "down") | |||
| 141 | - | ||||
| 142 | - | // Set up widget bounds for focus calculations | |||
| 143 | - | webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |||
| 144 | - | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |||
| 145 | - | webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 100}) | |||
| 146 | - | ||||
| 147 | - | wm.currentlyFocused = wm.root | |||
| 148 | - | ||||
| 149 | - | b.ResetTimer() | |||
| 150 | - | for i := 0; i < b.N; i++ { | |||
| 151 | - | wm.focusAdjacent("right") | |||
| 152 | - | wm.focusAdjacent("left") | |||
| 153 | - | } | |||
| 154 | - | } | |||
| 155 | - | ||||
| 156 | - | func BenchmarkStructuralNeighbor(b *testing.B) { | |||
| 157 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 158 | - | ||||
| 159 | - | // Create a complex nested structure | |||
| 160 | - | right, _ := wm.splitNode(wm.root, "right") | |||
| 161 | - | _, _ = wm.splitNode(right, "down") | |||
| 162 | - | _, _ = wm.splitNode(wm.root, "down") | |||
| 163 | - | ||||
| 164 | - | // Set up geometry | |||
| 165 | - | webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |||
| 166 | - | ||||
| 167 | - | b.ResetTimer() | |||
| 168 | - | for i := 0; i < b.N; i++ { | |||
| 169 | - | wm.structuralNeighbor(wm.root, "right") | |||
| 170 | - | wm.structuralNeighbor(wm.root, "down") | |||
| 171 | - | } | |||
| 172 | - | } | |||
| 173 | - | ||||
| 174 | - | func BenchmarkClosestLeafFromSubtree(b *testing.B) { | |||
| 175 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 176 | - | ||||
| 177 | - | // Build a subtree with multiple leaves | |||
| 178 | - | right, _ := wm.splitNode(wm.root, "right") | |||
| 179 | - | _, _ = wm.splitNode(right, "down") | |||
| 180 | - | _, _ = wm.splitNode(right, "up") | |||
| 181 | - | ||||
| 182 | - | // Set up geometry for all nodes | |||
| 183 | - | leaves := wm.collectLeavesFrom(right) | |||
| 184 | - | for i, leaf := range leaves { | |||
| 185 | - | webkit.SetWidgetBoundsForTesting(leaf.container, webkit.WidgetBounds{ | |||
| 186 | - | X: float64(i * 50), Y: 0, Width: 45, Height: 100, | |||
| 187 | - | }) | |||
| 188 | - | } | |||
| 189 | - | ||||
| 190 | - | b.ResetTimer() | |||
| 191 | - | for i := 0; i < b.N; i++ { | |||
| 192 | - | wm.closestLeafFromSubtree(right, 25, 50, "right") | |||
| 193 | - | } | |||
| 194 | - | } | |||
| 195 | - | ||||
| 196 | - | func BenchmarkComplexTreeOperations(b *testing.B) { | |||
| 197 | - | b.Run("SplitAndClose", func(b *testing.B) { | |||
| 198 | - | for i := 0; i < b.N; i++ { | |||
| 199 | - | b.StopTimer() | |||
| 200 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 201 | - | b.StartTimer() | |||
| 202 | - | ||||
| 203 | - | // Split multiple times then close in reverse order | |||
| 204 | - | nodes := []*paneNode{wm.root} | |||
| 205 | - | for j := 0; j < 5; j++ { | |||
| 206 | - | newNode, err := wm.splitNode(nodes[len(nodes)-1], "right") | |||
| 207 | - | if err != nil { | |||
| 208 | - | b.Fatalf("Split failed: %v", err) | |||
| 209 | - | } | |||
| 210 | - | nodes = append(nodes, newNode) | |||
| 211 | - | } | |||
| 212 | - | ||||
| 213 | - | // Close all but root | |||
| 214 | - | for j := len(nodes) - 1; j > 0; j-- { | |||
| 215 | - | err := wm.closePane(nodes[j]) | |||
| 216 | - | if err != nil { | |||
| 217 | - | b.Fatalf("Close failed: %v", err) | |||
| 218 | - | } | |||
| 219 | - | } | |||
| 220 | - | } | |||
| 221 | - | }) | |||
| 222 | - | ||||
| 223 | - | b.Run("BuildBalancedTree", func(b *testing.B) { | |||
| 224 | - | for i := 0; i < b.N; i++ { | |||
| 225 | - | b.StopTimer() | |||
| 226 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 227 | - | b.StartTimer() | |||
| 228 | - | ||||
| 229 | - | // Build a balanced tree by alternating split directions | |||
| 230 | - | leaves := []*paneNode{wm.root} | |||
| 231 | - | directions := []string{"right", "down", "left", "up"} | |||
| 232 | - | ||||
| 233 | - | for j := 0; j < 15 && len(leaves) > 0; j++ { | |||
| 234 | - | target := leaves[j%len(leaves)] | |||
| 235 | - | direction := directions[j%len(directions)] | |||
| 236 | - | ||||
| 237 | - | newNode, err := wm.splitNode(target, direction) | |||
| 238 | - | if err != nil { | |||
| 239 | - | continue | |||
| 240 | - | } | |||
| 241 | - | leaves = append(leaves, newNode) | |||
| 242 | - | } | |||
| 243 | - | } | |||
| 244 | - | }) | |||
| 245 | - | } | |||
| 246 | - | ||||
| 247 | - | func BenchmarkMemoryOperations(b *testing.B) { | |||
| 248 | - | b.Run("ViewToNodeMapping", func(b *testing.B) { | |||
| 249 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 250 | - | ||||
| 251 | - | // Create many nodes to stress the map | |||
| 252 | - | for i := 0; i < 100; i++ { | |||
| 253 | - | leaves := wm.collectLeaves() | |||
| 254 | - | if len(leaves) > 0 { | |||
| 255 | - | _, _ = wm.splitNode(leaves[0], "right") | |||
| 256 | - | } | |||
| 257 | - | } | |||
| 258 | - | ||||
| 259 | - | b.ResetTimer() | |||
| 260 | - | for i := 0; i < b.N; i++ { | |||
| 261 | - | // Access all mappings | |||
| 262 | - | for view, node := range wm.viewToNode { | |||
| 263 | - | _ = view | |||
| 264 | - | _ = node | |||
| 265 | - | } | |||
| 266 | - | } | |||
| 267 | - | }) | |||
| 268 | - | ||||
| 269 | - | b.Run("TreeWalkMemory", func(b *testing.B) { | |||
| 270 | - | wm := newTestWorkspaceManagerWithMocks(b) | |||
| 271 | - | ||||
| 272 | - | // Build deep tree | |||
| 273 | - | current := wm.root | |||
| 274 | - | for i := 0; i < 50; i++ { | |||
| 275 | - | newNode, err := wm.splitNode(current, "right") | |||
| 276 | - | if err != nil { | |||
| 277 | - | break | |||
| 278 | - | } | |||
| 279 | - | current = newNode | |||
| 280 | - | } | |||
| 281 | - | ||||
| 282 | - | b.ResetTimer() | |||
| 283 | - | for i := 0; i < b.N; i++ { | |||
| 284 | - | // Multiple tree operations that allocate | |||
| 285 | - | leaves := wm.collectLeaves() | |||
| 286 | - | _ = leaves | |||
| 287 | - | countNodes(wm.root) | |||
| 288 | - | wm.leftmostLeaf(wm.root) | |||
| 289 | - | } | |||
| 290 | - | }) | |||
| 291 | - | } | |||
| 292 | - | ||||
| 293 | - | // Helper function for benchmarks | |||
| 294 | - | func newTestWorkspaceManagerWithMocks(tb testing.TB) *WorkspaceManager { | |||
| 295 | - | tb.Helper() | |||
| 296 | - | webkit.ResetWidgetStubsForTesting() | |||
| 297 | - | ||||
| 298 | - | rootView, err := webkit.NewWebView(&webkit.Config{}) | |||
| 299 | - | if err != nil { | |||
| 300 | - | tb.Fatalf("failed to create root webview: %v", err) | |||
| 301 | - | } | |||
| 302 | - | ||||
| 303 | - | rootPane := &BrowserPane{ | |||
| 304 | - | webView: rootView, | |||
| 305 | - | id: "root", | |||
| 306 | - | } | |||
| 307 | - | ||||
| 308 | - | app := &BrowserApp{ | |||
| 309 | - | panes: []*BrowserPane{rootPane}, | |||
| 310 | - | activePane: rootPane, | |||
| 311 | - | webView: rootView, | |||
| 312 | - | } | |||
| 313 | - | ||||
| 314 | - | wm := &WorkspaceManager{ | |||
| 315 | - | app: app, | |||
| 316 | - | window: rootView.Window(), | |||
| 317 | - | viewToNode: make(map[*webkit.WebView]*paneNode), | |||
| 318 | - | lastSplitMsg: make(map[*webkit.WebView]time.Time), | |||
| 319 | - | lastExitMsg: make(map[*webkit.WebView]time.Time), | |||
| 320 | - | } | |||
| 321 | - | ||||
| 322 | - | wm.createWebViewFn = func() (*webkit.WebView, error) { | |||
| 323 | - | return webkit.NewWebView(&webkit.Config{}) | |||
| 324 | - | } | |||
| 325 | - | ||||
| 326 | - | wm.createPaneFn = func(view *webkit.WebView) (*BrowserPane, error) { | |||
| 327 | - | return &BrowserPane{ | |||
| 328 | - | webView: view, | |||
| 329 | - | id: "bench-pane", | |||
| 330 | - | }, nil | |||
| 331 | - | } | |||
| 332 | - | ||||
| 333 | - | root := &paneNode{ | |||
| 334 | - | pane: rootPane, | |||
| 335 | - | container: rootView.RootWidget(), | |||
| 336 | - | isLeaf: true, | |||
| 337 | - | } | |||
| 338 | - | wm.root = root | |||
| 339 | - | wm.currentlyFocused = root | |||
| 340 | - | wm.mainPane = root | |||
| 341 | - | wm.viewToNode[rootView] = root | |||
| 342 | - | ||||
| 343 | - | return wm | |||
| 344 | - | } | |||
D internal/app/browser/workspace_manager_recursion_test.go +0 -172
| @@ -1,172 +0,0 @@ | |||
| 1 | - | package browser | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | "time" | |
| 6 | - | ||
| 7 | - | "github.com/bnema/dumber/pkg/webkit" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | func TestInfiniteRecursionProtection(t *testing.T) { | |
| 11 | - | t.Run("BoundaryFallback with deep tree", func(t *testing.T) { | |
| 12 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |
| 13 | - | ||
| 14 | - | // Create a pathologically deep tree | |
| 15 | - | current := wm.root | |
| 16 | - | for i := 0; i < 20; i++ { | |
| 17 | - | newNode, err := wm.splitNode(current, "right") | |
| 18 | - | if err != nil { | |
| 19 | - | t.Fatalf("Split %d failed: %v", i, err) | |
| 20 | - | } | |
| 21 | - | current = newNode | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | // Test that boundaryFallback doesn't hang even with deep trees | |
| 25 | - | done := make(chan bool, 1) | |
| 26 | - | go func() { | |
| 27 | - | // This should complete quickly without infinite recursion | |
| 28 | - | result := wm.boundaryFallback(wm.root, "right") | |
| 29 | - | _ = result // We just care that it returns | |
| 30 | - | done <- true | |
| 31 | - | }() | |
| 32 | - | ||
| 33 | - | select { | |
| 34 | - | case <-done: | |
| 35 | - | // Good - function completed | |
| 36 | - | case <-time.After(1 * time.Second): | |
| 37 | - | t.Fatal("boundaryFallback took too long - likely infinite recursion") | |
| 38 | - | } | |
| 39 | - | }) | |
| 40 | - | ||
| 41 | - | t.Run("CollectLeaves with complex tree", func(t *testing.T) { | |
| 42 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |
| 43 | - | ||
| 44 | - | // Build a complex tree that could potentially cause issues | |
| 45 | - | leaves := []*paneNode{wm.root} | |
| 46 | - | for i := 0; i < 15; i++ { | |
| 47 | - | if len(leaves) > 0 { | |
| 48 | - | target := leaves[i%len(leaves)] | |
| 49 | - | newNode, err := wm.splitNode(target, "right") | |
| 50 | - | if err != nil { | |
| 51 | - | continue | |
| 52 | - | } | |
| 53 | - | leaves = append(leaves, newNode) | |
| 54 | - | } | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | // Test that collectLeaves doesn't hang | |
| 58 | - | done := make(chan []*paneNode, 1) | |
| 59 | - | go func() { | |
| 60 | - | result := wm.collectLeaves() | |
| 61 | - | done <- result | |
| 62 | - | }() | |
| 63 | - | ||
| 64 | - | select { | |
| 65 | - | case result := <-done: | |
| 66 | - | // Verify we got a reasonable result | |
| 67 | - | if len(result) == 0 { | |
| 68 | - | t.Error("Expected some leaves to be collected") | |
| 69 | - | } | |
| 70 | - | t.Logf("Collected %d leaves from complex tree", len(result)) | |
| 71 | - | case <-time.After(1 * time.Second): | |
| 72 | - | t.Fatal("collectLeaves took too long - likely infinite recursion") | |
| 73 | - | } | |
| 74 | - | }) | |
| 75 | - | ||
| 76 | - | t.Run("Focus calculation performance", func(t *testing.T) { | |
| 77 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |
| 78 | - | ||
| 79 | - | // Create a tree with geometry | |
| 80 | - | right, err := wm.splitNode(wm.root, "right") | |
| 81 | - | if err != nil { | |
| 82 | - | t.Fatalf("Failed to split right: %v", err) | |
| 83 | - | } | |
| 84 | - | bottom, err := wm.splitNode(right, "down") | |
| 85 | - | if err != nil { | |
| 86 | - | t.Fatalf("Failed to split down: %v", err) | |
| 87 | - | } | |
| 88 | - | ||
| 89 | - | // Set widget bounds | |
| 90 | - | webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |
| 91 | - | if right != nil { | |
| 92 | - | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |
| 93 | - | } | |
| 94 | - | if bottom != nil { | |
| 95 | - | webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 100}) | |
| 96 | - | } | |
| 97 | - | ||
| 98 | - | // Test that focus neighbor calculations complete quickly | |
| 99 | - | start := time.Now() | |
| 100 | - | ||
| 101 | - | // Perform multiple focus operations | |
| 102 | - | for i := 0; i < 10; i++ { | |
| 103 | - | wm.FocusNeighbor("right") | |
| 104 | - | wm.FocusNeighbor("left") | |
| 105 | - | wm.FocusNeighbor("down") | |
| 106 | - | wm.FocusNeighbor("up") | |
| 107 | - | } | |
| 108 | - | ||
| 109 | - | elapsed := time.Since(start) | |
| 110 | - | if elapsed > 100*time.Millisecond { | |
| 111 | - | t.Errorf("Focus operations took too long: %v", elapsed) | |
| 112 | - | } | |
| 113 | - | t.Logf("40 focus operations completed in %v", elapsed) | |
| 114 | - | }) | |
| 115 | - | ||
| 116 | - | t.Run("StructuralNeighbor depth protection", func(t *testing.T) { | |
| 117 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |
| 118 | - | ||
| 119 | - | // Create nested structure | |
| 120 | - | node1, _ := wm.splitNode(wm.root, "right") | |
| 121 | - | node2, _ := wm.splitNode(node1, "down") | |
| 122 | - | node3, _ := wm.splitNode(node2, "right") | |
| 123 | - | ||
| 124 | - | // Set some bounds for the calculation | |
| 125 | - | webkit.SetWidgetBoundsForTesting(node3.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |
| 126 | - | ||
| 127 | - | // Test structural neighbor calculation doesn't hang | |
| 128 | - | done := make(chan *paneNode, 1) | |
| 129 | - | go func() { | |
| 130 | - | result := wm.structuralNeighbor(node3, "left") | |
| 131 | - | done <- result | |
| 132 | - | }() | |
| 133 | - | ||
| 134 | - | select { | |
| 135 | - | case result := <-done: | |
| 136 | - | t.Logf("Structural neighbor calculation completed, result: %v", result != nil) | |
| 137 | - | case <-time.After(500 * time.Millisecond): | |
| 138 | - | t.Fatal("structuralNeighbor took too long") | |
| 139 | - | } | |
| 140 | - | }) | |
| 141 | - | } | |
| 142 | - | ||
| 143 | - | func TestRecursionLimits(t *testing.T) { | |
| 144 | - | t.Run("MaxDepth protection", func(t *testing.T) { | |
| 145 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |
| 146 | - | ||
| 147 | - | // Create an artificially corrupted node for testing | |
| 148 | - | // (This would never happen in normal operation) | |
| 149 | - | corruptedNode := &paneNode{ | |
| 150 | - | isLeaf: false, | |
| 151 | - | left: nil, // This creates a problematic state | |
| 152 | - | right: nil, // No children but not a leaf | |
| 153 | - | } | |
| 154 | - | ||
| 155 | - | // Test that boundaryFallback handles this gracefully | |
| 156 | - | result := wm.boundaryFallback(corruptedNode, "right") | |
| 157 | - | if result != nil { | |
| 158 | - | t.Error("Expected nil result for corrupted node") | |
| 159 | - | } | |
| 160 | - | ||
| 161 | - | // The function should return quickly, not hang | |
| 162 | - | start := time.Now() | |
| 163 | - | for i := 0; i < 10; i++ { | |
| 164 | - | wm.boundaryFallback(corruptedNode, "left") | |
| 165 | - | } | |
| 166 | - | elapsed := time.Since(start) | |
| 167 | - | ||
| 168 | - | if elapsed > 10*time.Millisecond { | |
| 169 | - | t.Errorf("Corrupted node handling took too long: %v", elapsed) | |
| 170 | - | } | |
| 171 | - | }) | |
| 172 | - | } | |
| @@ -1,172 +0,0 @@ | |||||
| 1 | - | package browser | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "testing" | |||
| 5 | - | "time" | |||
| 6 | - | ||||
| 7 | - | "github.com/bnema/dumber/pkg/webkit" | |||
| 8 | - | ) | |||
| 9 | - | ||||
| 10 | - | func TestInfiniteRecursionProtection(t *testing.T) { | |||
| 11 | - | t.Run("BoundaryFallback with deep tree", func(t *testing.T) { | |||
| 12 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |||
| 13 | - | ||||
| 14 | - | // Create a pathologically deep tree | |||
| 15 | - | current := wm.root | |||
| 16 | - | for i := 0; i < 20; i++ { | |||
| 17 | - | newNode, err := wm.splitNode(current, "right") | |||
| 18 | - | if err != nil { | |||
| 19 | - | t.Fatalf("Split %d failed: %v", i, err) | |||
| 20 | - | } | |||
| 21 | - | current = newNode | |||
| 22 | - | } | |||
| 23 | - | ||||
| 24 | - | // Test that boundaryFallback doesn't hang even with deep trees | |||
| 25 | - | done := make(chan bool, 1) | |||
| 26 | - | go func() { | |||
| 27 | - | // This should complete quickly without infinite recursion | |||
| 28 | - | result := wm.boundaryFallback(wm.root, "right") | |||
| 29 | - | _ = result // We just care that it returns | |||
| 30 | - | done <- true | |||
| 31 | - | }() | |||
| 32 | - | ||||
| 33 | - | select { | |||
| 34 | - | case <-done: | |||
| 35 | - | // Good - function completed | |||
| 36 | - | case <-time.After(1 * time.Second): | |||
| 37 | - | t.Fatal("boundaryFallback took too long - likely infinite recursion") | |||
| 38 | - | } | |||
| 39 | - | }) | |||
| 40 | - | ||||
| 41 | - | t.Run("CollectLeaves with complex tree", func(t *testing.T) { | |||
| 42 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |||
| 43 | - | ||||
| 44 | - | // Build a complex tree that could potentially cause issues | |||
| 45 | - | leaves := []*paneNode{wm.root} | |||
| 46 | - | for i := 0; i < 15; i++ { | |||
| 47 | - | if len(leaves) > 0 { | |||
| 48 | - | target := leaves[i%len(leaves)] | |||
| 49 | - | newNode, err := wm.splitNode(target, "right") | |||
| 50 | - | if err != nil { | |||
| 51 | - | continue | |||
| 52 | - | } | |||
| 53 | - | leaves = append(leaves, newNode) | |||
| 54 | - | } | |||
| 55 | - | } | |||
| 56 | - | ||||
| 57 | - | // Test that collectLeaves doesn't hang | |||
| 58 | - | done := make(chan []*paneNode, 1) | |||
| 59 | - | go func() { | |||
| 60 | - | result := wm.collectLeaves() | |||
| 61 | - | done <- result | |||
| 62 | - | }() | |||
| 63 | - | ||||
| 64 | - | select { | |||
| 65 | - | case result := <-done: | |||
| 66 | - | // Verify we got a reasonable result | |||
| 67 | - | if len(result) == 0 { | |||
| 68 | - | t.Error("Expected some leaves to be collected") | |||
| 69 | - | } | |||
| 70 | - | t.Logf("Collected %d leaves from complex tree", len(result)) | |||
| 71 | - | case <-time.After(1 * time.Second): | |||
| 72 | - | t.Fatal("collectLeaves took too long - likely infinite recursion") | |||
| 73 | - | } | |||
| 74 | - | }) | |||
| 75 | - | ||||
| 76 | - | t.Run("Focus calculation performance", func(t *testing.T) { | |||
| 77 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |||
| 78 | - | ||||
| 79 | - | // Create a tree with geometry | |||
| 80 | - | right, err := wm.splitNode(wm.root, "right") | |||
| 81 | - | if err != nil { | |||
| 82 | - | t.Fatalf("Failed to split right: %v", err) | |||
| 83 | - | } | |||
| 84 | - | bottom, err := wm.splitNode(right, "down") | |||
| 85 | - | if err != nil { | |||
| 86 | - | t.Fatalf("Failed to split down: %v", err) | |||
| 87 | - | } | |||
| 88 | - | ||||
| 89 | - | // Set widget bounds | |||
| 90 | - | webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |||
| 91 | - | if right != nil { | |||
| 92 | - | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |||
| 93 | - | } | |||
| 94 | - | if bottom != nil { | |||
| 95 | - | webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 100}) | |||
| 96 | - | } | |||
| 97 | - | ||||
| 98 | - | // Test that focus neighbor calculations complete quickly | |||
| 99 | - | start := time.Now() | |||
| 100 | - | ||||
| 101 | - | // Perform multiple focus operations | |||
| 102 | - | for i := 0; i < 10; i++ { | |||
| 103 | - | wm.FocusNeighbor("right") | |||
| 104 | - | wm.FocusNeighbor("left") | |||
| 105 | - | wm.FocusNeighbor("down") | |||
| 106 | - | wm.FocusNeighbor("up") | |||
| 107 | - | } | |||
| 108 | - | ||||
| 109 | - | elapsed := time.Since(start) | |||
| 110 | - | if elapsed > 100*time.Millisecond { | |||
| 111 | - | t.Errorf("Focus operations took too long: %v", elapsed) | |||
| 112 | - | } | |||
| 113 | - | t.Logf("40 focus operations completed in %v", elapsed) | |||
| 114 | - | }) | |||
| 115 | - | ||||
| 116 | - | t.Run("StructuralNeighbor depth protection", func(t *testing.T) { | |||
| 117 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |||
| 118 | - | ||||
| 119 | - | // Create nested structure | |||
| 120 | - | node1, _ := wm.splitNode(wm.root, "right") | |||
| 121 | - | node2, _ := wm.splitNode(node1, "down") | |||
| 122 | - | node3, _ := wm.splitNode(node2, "right") | |||
| 123 | - | ||||
| 124 | - | // Set some bounds for the calculation | |||
| 125 | - | webkit.SetWidgetBoundsForTesting(node3.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |||
| 126 | - | ||||
| 127 | - | // Test structural neighbor calculation doesn't hang | |||
| 128 | - | done := make(chan *paneNode, 1) | |||
| 129 | - | go func() { | |||
| 130 | - | result := wm.structuralNeighbor(node3, "left") | |||
| 131 | - | done <- result | |||
| 132 | - | }() | |||
| 133 | - | ||||
| 134 | - | select { | |||
| 135 | - | case result := <-done: | |||
| 136 | - | t.Logf("Structural neighbor calculation completed, result: %v", result != nil) | |||
| 137 | - | case <-time.After(500 * time.Millisecond): | |||
| 138 | - | t.Fatal("structuralNeighbor took too long") | |||
| 139 | - | } | |||
| 140 | - | }) | |||
| 141 | - | } | |||
| 142 | - | ||||
| 143 | - | func TestRecursionLimits(t *testing.T) { | |||
| 144 | - | t.Run("MaxDepth protection", func(t *testing.T) { | |||
| 145 | - | wm := newTestWorkspaceManagerWithMocksForTree(t) | |||
| 146 | - | ||||
| 147 | - | // Create an artificially corrupted node for testing | |||
| 148 | - | // (This would never happen in normal operation) | |||
| 149 | - | corruptedNode := &paneNode{ | |||
| 150 | - | isLeaf: false, | |||
| 151 | - | left: nil, // This creates a problematic state | |||
| 152 | - | right: nil, // No children but not a leaf | |||
| 153 | - | } | |||
| 154 | - | ||||
| 155 | - | // Test that boundaryFallback handles this gracefully | |||
| 156 | - | result := wm.boundaryFallback(corruptedNode, "right") | |||
| 157 | - | if result != nil { | |||
| 158 | - | t.Error("Expected nil result for corrupted node") | |||
| 159 | - | } | |||
| 160 | - | ||||
| 161 | - | // The function should return quickly, not hang | |||
| 162 | - | start := time.Now() | |||
| 163 | - | for i := 0; i < 10; i++ { | |||
| 164 | - | wm.boundaryFallback(corruptedNode, "left") | |||
| 165 | - | } | |||
| 166 | - | elapsed := time.Since(start) | |||
| 167 | - | ||||
| 168 | - | if elapsed > 10*time.Millisecond { | |||
| 169 | - | t.Errorf("Corrupted node handling took too long: %v", elapsed) | |||
| 170 | - | } | |||
| 171 | - | }) | |||
| 172 | - | } | |||
D internal/app/browser/workspace_manager_test.go +0 -273
| @@ -1,273 +0,0 @@ | |||
| 1 | - | package browser | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | ||
| 6 | - | "github.com/bnema/dumber/pkg/webkit" | |
| 7 | - | ) | |
| 8 | - | ||
| 9 | - | func newTestWorkspaceManager(t *testing.T) *WorkspaceManager { | |
| 10 | - | t.Helper() | |
| 11 | - | webkit.ResetWidgetStubsForTesting() | |
| 12 | - | ||
| 13 | - | rootView, err := webkit.NewWebView(&webkit.Config{}) | |
| 14 | - | if err != nil { | |
| 15 | - | t.Fatalf("failed to create root webview: %v", err) | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | rootPane := &BrowserPane{webView: rootView} | |
| 19 | - | ||
| 20 | - | // Create mock window shortcut handler | |
| 21 | - | mockWindowShortcutHandler := &mockWindowShortcutHandler{} | |
| 22 | - | ||
| 23 | - | app := &BrowserApp{} | |
| 24 | - | app.panes = []*BrowserPane{rootPane} | |
| 25 | - | app.activePane = rootPane | |
| 26 | - | app.webView = rootView | |
| 27 | - | app.windowShortcutHandler = mockWindowShortcutHandler | |
| 28 | - | ||
| 29 | - | wm := NewWorkspaceManager(app, rootPane) | |
| 30 | - | wm.createWebViewFn = func() (*webkit.WebView, error) { | |
| 31 | - | return webkit.NewWebView(&webkit.Config{}) | |
| 32 | - | } | |
| 33 | - | wm.createPaneFn = func(view *webkit.WebView) (*BrowserPane, error) { | |
| 34 | - | return &BrowserPane{webView: view}, nil | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | return wm | |
| 38 | - | } | |
| 39 | - | ||
| 40 | - | func TestSplitNodeCreatesExpectedTree(t *testing.T) { | |
| 41 | - | cases := []struct { | |
| 42 | - | name string | |
| 43 | - | direction string | |
| 44 | - | expectOrientation webkit.Orientation | |
| 45 | - | existingIsStart bool | |
| 46 | - | }{ | |
| 47 | - | {name: "Right", direction: "right", expectOrientation: webkit.OrientationHorizontal, existingIsStart: true}, | |
| 48 | - | {name: "Left", direction: "left", expectOrientation: webkit.OrientationHorizontal, existingIsStart: false}, | |
| 49 | - | {name: "Up", direction: "up", expectOrientation: webkit.OrientationVertical, existingIsStart: false}, | |
| 50 | - | {name: "Down", direction: "down", expectOrientation: webkit.OrientationVertical, existingIsStart: true}, | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | for _, tc := range cases { | |
| 54 | - | t.Run(tc.name, func(t *testing.T) { | |
| 55 | - | wm := newTestWorkspaceManager(t) | |
| 56 | - | original := wm.currentlyFocused | |
| 57 | - | ||
| 58 | - | newLeaf, err := wm.splitNode(original, tc.direction) | |
| 59 | - | if err != nil { | |
| 60 | - | t.Fatalf("splitNode(%s) returned error: %v", tc.direction, err) | |
| 61 | - | } | |
| 62 | - | ||
| 63 | - | parent := newLeaf.parent | |
| 64 | - | if parent == nil { | |
| 65 | - | t.Fatalf("expected new leaf to have parent") | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | if parent.orientation != tc.expectOrientation { | |
| 69 | - | t.Fatalf("expected orientation %v got %v", tc.expectOrientation, parent.orientation) | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | if tc.existingIsStart { | |
| 73 | - | if parent.left != original { | |
| 74 | - | t.Fatalf("expected original pane to remain as start child") | |
| 75 | - | } | |
| 76 | - | if parent.right != newLeaf { | |
| 77 | - | t.Fatalf("expected new pane to be end child") | |
| 78 | - | } | |
| 79 | - | } else { | |
| 80 | - | if parent.left != newLeaf { | |
| 81 | - | t.Fatalf("expected new pane to be start child") | |
| 82 | - | } | |
| 83 | - | if parent.right != original { | |
| 84 | - | t.Fatalf("expected original pane to be end child") | |
| 85 | - | } | |
| 86 | - | } | |
| 87 | - | ||
| 88 | - | if wm.root != parent { | |
| 89 | - | t.Fatalf("expected parent to become new root") | |
| 90 | - | } | |
| 91 | - | ||
| 92 | - | if len(wm.app.panes) != 2 { | |
| 93 | - | t.Fatalf("expected 2 panes registered, got %d", len(wm.app.panes)) | |
| 94 | - | } | |
| 95 | - | ||
| 96 | - | mapped := wm.viewToNode[newLeaf.pane.webView] | |
| 97 | - | if mapped != newLeaf { | |
| 98 | - | t.Fatalf("expected new webview to map to new leaf") | |
| 99 | - | } | |
| 100 | - | }) | |
| 101 | - | } | |
| 102 | - | } | |
| 103 | - | ||
| 104 | - | func TestClosePanePromotesSibling(t *testing.T) { | |
| 105 | - | wm := newTestWorkspaceManager(t) | |
| 106 | - | original := wm.currentlyFocused | |
| 107 | - | ||
| 108 | - | newLeaf, err := wm.splitNode(original, "right") | |
| 109 | - | if err != nil { | |
| 110 | - | t.Fatalf("split failed: %v", err) | |
| 111 | - | } | |
| 112 | - | ||
| 113 | - | if err := wm.closePane(newLeaf); err != nil { | |
| 114 | - | t.Fatalf("closePane failed: %v", err) | |
| 115 | - | } | |
| 116 | - | ||
| 117 | - | if wm.root != original { | |
| 118 | - | t.Fatalf("expected original leaf to become root after closing sibling") | |
| 119 | - | } | |
| 120 | - | ||
| 121 | - | if original.parent != nil { | |
| 122 | - | t.Fatalf("expected original leaf to have no parent after promotion") | |
| 123 | - | } | |
| 124 | - | ||
| 125 | - | if len(wm.app.panes) != 1 { | |
| 126 | - | t.Fatalf("expected single pane remaining, got %d", len(wm.app.panes)) | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | if wm.app.panes[0] != original.pane { | |
| 130 | - | t.Fatalf("expected remaining pane to match original") | |
| 131 | - | } | |
| 132 | - | ||
| 133 | - | if wm.currentlyFocused != original { | |
| 134 | - | t.Fatalf("expected focus to move to promoted pane") | |
| 135 | - | } | |
| 136 | - | } | |
| 137 | - | ||
| 138 | - | func TestFocusNeighborWithBounds(t *testing.T) { | |
| 139 | - | wm := newTestWorkspaceManager(t) | |
| 140 | - | left := wm.currentlyFocused | |
| 141 | - | ||
| 142 | - | right, err := wm.splitNode(left, "right") | |
| 143 | - | if err != nil { | |
| 144 | - | t.Fatalf("split failed: %v", err) | |
| 145 | - | } | |
| 146 | - | ||
| 147 | - | if right == nil { | |
| 148 | - | t.Fatalf("expected new right leaf") | |
| 149 | - | } | |
| 150 | - | ||
| 151 | - | // Provide geometry hints for focus calculations. | |
| 152 | - | webkit.SetWidgetBoundsForTesting(left.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |
| 153 | - | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |
| 154 | - | ||
| 155 | - | if !wm.FocusNeighbor("left") { | |
| 156 | - | t.Fatalf("expected focus neighbor left to succeed") | |
| 157 | - | } | |
| 158 | - | ||
| 159 | - | if wm.currentlyFocused != left { | |
| 160 | - | t.Fatalf("expected focus to move to left pane") | |
| 161 | - | } | |
| 162 | - | ||
| 163 | - | // Vertical split from the left pane to test up/down logic. | |
| 164 | - | bottom, err := wm.splitNode(left, "down") | |
| 165 | - | if err != nil { | |
| 166 | - | t.Fatalf("vertical split failed: %v", err) | |
| 167 | - | } | |
| 168 | - | ||
| 169 | - | top := bottom.parent.left | |
| 170 | - | ||
| 171 | - | webkit.SetWidgetBoundsForTesting(top.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 90}) | |
| 172 | - | webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 90}) | |
| 173 | - | ||
| 174 | - | // Active pane should be bottom after split. Move focus up. | |
| 175 | - | if wm.currentlyFocused != bottom { | |
| 176 | - | t.Fatalf("expected bottom pane to be active after split") | |
| 177 | - | } | |
| 178 | - | ||
| 179 | - | if !wm.FocusNeighbor("up") { | |
| 180 | - | t.Fatalf("expected focus neighbor up to succeed") | |
| 181 | - | } | |
| 182 | - | ||
| 183 | - | if wm.currentlyFocused != top { | |
| 184 | - | t.Fatalf("expected focus to move to top pane") | |
| 185 | - | } | |
| 186 | - | } | |
| 187 | - | ||
| 188 | - | func TestFocusNeighborWithoutPeer(t *testing.T) { | |
| 189 | - | wm := newTestWorkspaceManager(t) | |
| 190 | - | if wm.FocusNeighbor("left") { | |
| 191 | - | t.Fatalf("expected focus move to fail when only one pane exists") | |
| 192 | - | } | |
| 193 | - | } | |
| 194 | - | ||
| 195 | - | // TestWorkspaceNavigationAfterFocusChanges tests that workspace navigation | |
| 196 | - | // works correctly after focus changes between panes. This validates that | |
| 197 | - | // the FocusNeighbor functionality works regardless of which pane previously | |
| 198 | - | // had focus (since Alt+Arrow shortcuts are now handled globally). | |
| 199 | - | func TestWorkspaceNavigationAfterFocusChanges(t *testing.T) { | |
| 200 | - | wm := newTestWorkspaceManager(t) | |
| 201 | - | ||
| 202 | - | // Split to create two panes | |
| 203 | - | right, err := wm.splitNode(wm.root, "right") | |
| 204 | - | if err != nil { | |
| 205 | - | t.Fatalf("split failed: %v", err) | |
| 206 | - | } | |
| 207 | - | ||
| 208 | - | left := right.parent.left | |
| 209 | - | ||
| 210 | - | // Set up geometry for navigation calculations | |
| 211 | - | webkit.SetWidgetBoundsForTesting(left.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |
| 212 | - | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |
| 213 | - | ||
| 214 | - | // Initially, the active pane should be the right one (newly created) | |
| 215 | - | if wm.GetActiveNode() != right { | |
| 216 | - | t.Fatalf("expected right pane to be active initially") | |
| 217 | - | } | |
| 218 | - | ||
| 219 | - | // Test that navigation works from right to left | |
| 220 | - | if !wm.FocusNeighbor("left") { | |
| 221 | - | t.Fatalf("expected focus neighbor left to succeed from right pane") | |
| 222 | - | } | |
| 223 | - | ||
| 224 | - | if wm.GetActiveNode() != left { | |
| 225 | - | t.Fatalf("expected focus to move to left pane") | |
| 226 | - | } | |
| 227 | - | ||
| 228 | - | // Test that navigation works from left to right | |
| 229 | - | if !wm.FocusNeighbor("right") { | |
| 230 | - | t.Fatalf("expected focus neighbor right to succeed from left pane") | |
| 231 | - | } | |
| 232 | - | ||
| 233 | - | if wm.GetActiveNode() != right { | |
| 234 | - | t.Fatalf("expected focus to move back to right pane") | |
| 235 | - | } | |
| 236 | - | ||
| 237 | - | // Simulate multiple focus changes to verify that workspace navigation | |
| 238 | - | // works consistently (the old bug was per-webview shortcut registration) | |
| 239 | - | for i := 0; i < 3; i++ { | |
| 240 | - | // Move focus left | |
| 241 | - | wm.currentlyFocused = left | |
| 242 | - | wm.focusManager.SetActivePane(left) | |
| 243 | - | if wm.GetActiveNode() != left { | |
| 244 | - | t.Fatalf("expected left pane to be active after focusNode call %d", i) | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | // Test navigation still works | |
| 248 | - | if !wm.FocusNeighbor("right") { | |
| 249 | - | t.Fatalf("expected navigation right to work after focus change %d", i) | |
| 250 | - | } | |
| 251 | - | ||
| 252 | - | // Move focus right | |
| 253 | - | wm.currentlyFocused = right | |
| 254 | - | wm.focusManager.SetActivePane(right) | |
| 255 | - | if wm.GetActiveNode() != right { | |
| 256 | - | t.Fatalf("expected right pane to be active after focusNode call %d", i) | |
| 257 | - | } | |
| 258 | - | ||
| 259 | - | // Test navigation still works | |
| 260 | - | if !wm.FocusNeighbor("left") { | |
| 261 | - | t.Fatalf("expected navigation left to work after focus change %d", i) | |
| 262 | - | } | |
| 263 | - | } | |
| 264 | - | ||
| 265 | - | // Final verification that we can navigate in both directions | |
| 266 | - | if !wm.FocusNeighbor("right") { | |
| 267 | - | t.Fatalf("expected final navigation right to work") | |
| 268 | - | } | |
| 269 | - | ||
| 270 | - | if wm.GetActiveNode() != right { | |
| 271 | - | t.Fatalf("expected final active pane to be right") | |
| 272 | - | } | |
| 273 | - | } | |
| @@ -1,273 +0,0 @@ | |||||
| 1 | - | package browser | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "testing" | |||
| 5 | - | ||||
| 6 | - | "github.com/bnema/dumber/pkg/webkit" | |||
| 7 | - | ) | |||
| 8 | - | ||||
| 9 | - | func newTestWorkspaceManager(t *testing.T) *WorkspaceManager { | |||
| 10 | - | t.Helper() | |||
| 11 | - | webkit.ResetWidgetStubsForTesting() | |||
| 12 | - | ||||
| 13 | - | rootView, err := webkit.NewWebView(&webkit.Config{}) | |||
| 14 | - | if err != nil { | |||
| 15 | - | t.Fatalf("failed to create root webview: %v", err) | |||
| 16 | - | } | |||
| 17 | - | ||||
| 18 | - | rootPane := &BrowserPane{webView: rootView} | |||
| 19 | - | ||||
| 20 | - | // Create mock window shortcut handler | |||
| 21 | - | mockWindowShortcutHandler := &mockWindowShortcutHandler{} | |||
| 22 | - | ||||
| 23 | - | app := &BrowserApp{} | |||
| 24 | - | app.panes = []*BrowserPane{rootPane} | |||
| 25 | - | app.activePane = rootPane | |||
| 26 | - | app.webView = rootView | |||
| 27 | - | app.windowShortcutHandler = mockWindowShortcutHandler | |||
| 28 | - | ||||
| 29 | - | wm := NewWorkspaceManager(app, rootPane) | |||
| 30 | - | wm.createWebViewFn = func() (*webkit.WebView, error) { | |||
| 31 | - | return webkit.NewWebView(&webkit.Config{}) | |||
| 32 | - | } | |||
| 33 | - | wm.createPaneFn = func(view *webkit.WebView) (*BrowserPane, error) { | |||
| 34 | - | return &BrowserPane{webView: view}, nil | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | return wm | |||
| 38 | - | } | |||
| 39 | - | ||||
| 40 | - | func TestSplitNodeCreatesExpectedTree(t *testing.T) { | |||
| 41 | - | cases := []struct { | |||
| 42 | - | name string | |||
| 43 | - | direction string | |||
| 44 | - | expectOrientation webkit.Orientation | |||
| 45 | - | existingIsStart bool | |||
| 46 | - | }{ | |||
| 47 | - | {name: "Right", direction: "right", expectOrientation: webkit.OrientationHorizontal, existingIsStart: true}, | |||
| 48 | - | {name: "Left", direction: "left", expectOrientation: webkit.OrientationHorizontal, existingIsStart: false}, | |||
| 49 | - | {name: "Up", direction: "up", expectOrientation: webkit.OrientationVertical, existingIsStart: false}, | |||
| 50 | - | {name: "Down", direction: "down", expectOrientation: webkit.OrientationVertical, existingIsStart: true}, | |||
| 51 | - | } | |||
| 52 | - | ||||
| 53 | - | for _, tc := range cases { | |||
| 54 | - | t.Run(tc.name, func(t *testing.T) { | |||
| 55 | - | wm := newTestWorkspaceManager(t) | |||
| 56 | - | original := wm.currentlyFocused | |||
| 57 | - | ||||
| 58 | - | newLeaf, err := wm.splitNode(original, tc.direction) | |||
| 59 | - | if err != nil { | |||
| 60 | - | t.Fatalf("splitNode(%s) returned error: %v", tc.direction, err) | |||
| 61 | - | } | |||
| 62 | - | ||||
| 63 | - | parent := newLeaf.parent | |||
| 64 | - | if parent == nil { | |||
| 65 | - | t.Fatalf("expected new leaf to have parent") | |||
| 66 | - | } | |||
| 67 | - | ||||
| 68 | - | if parent.orientation != tc.expectOrientation { | |||
| 69 | - | t.Fatalf("expected orientation %v got %v", tc.expectOrientation, parent.orientation) | |||
| 70 | - | } | |||
| 71 | - | ||||
| 72 | - | if tc.existingIsStart { | |||
| 73 | - | if parent.left != original { | |||
| 74 | - | t.Fatalf("expected original pane to remain as start child") | |||
| 75 | - | } | |||
| 76 | - | if parent.right != newLeaf { | |||
| 77 | - | t.Fatalf("expected new pane to be end child") | |||
| 78 | - | } | |||
| 79 | - | } else { | |||
| 80 | - | if parent.left != newLeaf { | |||
| 81 | - | t.Fatalf("expected new pane to be start child") | |||
| 82 | - | } | |||
| 83 | - | if parent.right != original { | |||
| 84 | - | t.Fatalf("expected original pane to be end child") | |||
| 85 | - | } | |||
| 86 | - | } | |||
| 87 | - | ||||
| 88 | - | if wm.root != parent { | |||
| 89 | - | t.Fatalf("expected parent to become new root") | |||
| 90 | - | } | |||
| 91 | - | ||||
| 92 | - | if len(wm.app.panes) != 2 { | |||
| 93 | - | t.Fatalf("expected 2 panes registered, got %d", len(wm.app.panes)) | |||
| 94 | - | } | |||
| 95 | - | ||||
| 96 | - | mapped := wm.viewToNode[newLeaf.pane.webView] | |||
| 97 | - | if mapped != newLeaf { | |||
| 98 | - | t.Fatalf("expected new webview to map to new leaf") | |||
| 99 | - | } | |||
| 100 | - | }) | |||
| 101 | - | } | |||
| 102 | - | } | |||
| 103 | - | ||||
| 104 | - | func TestClosePanePromotesSibling(t *testing.T) { | |||
| 105 | - | wm := newTestWorkspaceManager(t) | |||
| 106 | - | original := wm.currentlyFocused | |||
| 107 | - | ||||
| 108 | - | newLeaf, err := wm.splitNode(original, "right") | |||
| 109 | - | if err != nil { | |||
| 110 | - | t.Fatalf("split failed: %v", err) | |||
| 111 | - | } | |||
| 112 | - | ||||
| 113 | - | if err := wm.closePane(newLeaf); err != nil { | |||
| 114 | - | t.Fatalf("closePane failed: %v", err) | |||
| 115 | - | } | |||
| 116 | - | ||||
| 117 | - | if wm.root != original { | |||
| 118 | - | t.Fatalf("expected original leaf to become root after closing sibling") | |||
| 119 | - | } | |||
| 120 | - | ||||
| 121 | - | if original.parent != nil { | |||
| 122 | - | t.Fatalf("expected original leaf to have no parent after promotion") | |||
| 123 | - | } | |||
| 124 | - | ||||
| 125 | - | if len(wm.app.panes) != 1 { | |||
| 126 | - | t.Fatalf("expected single pane remaining, got %d", len(wm.app.panes)) | |||
| 127 | - | } | |||
| 128 | - | ||||
| 129 | - | if wm.app.panes[0] != original.pane { | |||
| 130 | - | t.Fatalf("expected remaining pane to match original") | |||
| 131 | - | } | |||
| 132 | - | ||||
| 133 | - | if wm.currentlyFocused != original { | |||
| 134 | - | t.Fatalf("expected focus to move to promoted pane") | |||
| 135 | - | } | |||
| 136 | - | } | |||
| 137 | - | ||||
| 138 | - | func TestFocusNeighborWithBounds(t *testing.T) { | |||
| 139 | - | wm := newTestWorkspaceManager(t) | |||
| 140 | - | left := wm.currentlyFocused | |||
| 141 | - | ||||
| 142 | - | right, err := wm.splitNode(left, "right") | |||
| 143 | - | if err != nil { | |||
| 144 | - | t.Fatalf("split failed: %v", err) | |||
| 145 | - | } | |||
| 146 | - | ||||
| 147 | - | if right == nil { | |||
| 148 | - | t.Fatalf("expected new right leaf") | |||
| 149 | - | } | |||
| 150 | - | ||||
| 151 | - | // Provide geometry hints for focus calculations. | |||
| 152 | - | webkit.SetWidgetBoundsForTesting(left.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |||
| 153 | - | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |||
| 154 | - | ||||
| 155 | - | if !wm.FocusNeighbor("left") { | |||
| 156 | - | t.Fatalf("expected focus neighbor left to succeed") | |||
| 157 | - | } | |||
| 158 | - | ||||
| 159 | - | if wm.currentlyFocused != left { | |||
| 160 | - | t.Fatalf("expected focus to move to left pane") | |||
| 161 | - | } | |||
| 162 | - | ||||
| 163 | - | // Vertical split from the left pane to test up/down logic. | |||
| 164 | - | bottom, err := wm.splitNode(left, "down") | |||
| 165 | - | if err != nil { | |||
| 166 | - | t.Fatalf("vertical split failed: %v", err) | |||
| 167 | - | } | |||
| 168 | - | ||||
| 169 | - | top := bottom.parent.left | |||
| 170 | - | ||||
| 171 | - | webkit.SetWidgetBoundsForTesting(top.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 90}) | |||
| 172 | - | webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 90}) | |||
| 173 | - | ||||
| 174 | - | // Active pane should be bottom after split. Move focus up. | |||
| 175 | - | if wm.currentlyFocused != bottom { | |||
| 176 | - | t.Fatalf("expected bottom pane to be active after split") | |||
| 177 | - | } | |||
| 178 | - | ||||
| 179 | - | if !wm.FocusNeighbor("up") { | |||
| 180 | - | t.Fatalf("expected focus neighbor up to succeed") | |||
| 181 | - | } | |||
| 182 | - | ||||
| 183 | - | if wm.currentlyFocused != top { | |||
| 184 | - | t.Fatalf("expected focus to move to top pane") | |||
| 185 | - | } | |||
| 186 | - | } | |||
| 187 | - | ||||
| 188 | - | func TestFocusNeighborWithoutPeer(t *testing.T) { | |||
| 189 | - | wm := newTestWorkspaceManager(t) | |||
| 190 | - | if wm.FocusNeighbor("left") { | |||
| 191 | - | t.Fatalf("expected focus move to fail when only one pane exists") | |||
| 192 | - | } | |||
| 193 | - | } | |||
| 194 | - | ||||
| 195 | - | // TestWorkspaceNavigationAfterFocusChanges tests that workspace navigation | |||
| 196 | - | // works correctly after focus changes between panes. This validates that | |||
| 197 | - | // the FocusNeighbor functionality works regardless of which pane previously | |||
| 198 | - | // had focus (since Alt+Arrow shortcuts are now handled globally). | |||
| 199 | - | func TestWorkspaceNavigationAfterFocusChanges(t *testing.T) { | |||
| 200 | - | wm := newTestWorkspaceManager(t) | |||
| 201 | - | ||||
| 202 | - | // Split to create two panes | |||
| 203 | - | right, err := wm.splitNode(wm.root, "right") | |||
| 204 | - | if err != nil { | |||
| 205 | - | t.Fatalf("split failed: %v", err) | |||
| 206 | - | } | |||
| 207 | - | ||||
| 208 | - | left := right.parent.left | |||
| 209 | - | ||||
| 210 | - | // Set up geometry for navigation calculations | |||
| 211 | - | webkit.SetWidgetBoundsForTesting(left.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |||
| 212 | - | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |||
| 213 | - | ||||
| 214 | - | // Initially, the active pane should be the right one (newly created) | |||
| 215 | - | if wm.GetActiveNode() != right { | |||
| 216 | - | t.Fatalf("expected right pane to be active initially") | |||
| 217 | - | } | |||
| 218 | - | ||||
| 219 | - | // Test that navigation works from right to left | |||
| 220 | - | if !wm.FocusNeighbor("left") { | |||
| 221 | - | t.Fatalf("expected focus neighbor left to succeed from right pane") | |||
| 222 | - | } | |||
| 223 | - | ||||
| 224 | - | if wm.GetActiveNode() != left { | |||
| 225 | - | t.Fatalf("expected focus to move to left pane") | |||
| 226 | - | } | |||
| 227 | - | ||||
| 228 | - | // Test that navigation works from left to right | |||
| 229 | - | if !wm.FocusNeighbor("right") { | |||
| 230 | - | t.Fatalf("expected focus neighbor right to succeed from left pane") | |||
| 231 | - | } | |||
| 232 | - | ||||
| 233 | - | if wm.GetActiveNode() != right { | |||
| 234 | - | t.Fatalf("expected focus to move back to right pane") | |||
| 235 | - | } | |||
| 236 | - | ||||
| 237 | - | // Simulate multiple focus changes to verify that workspace navigation | |||
| 238 | - | // works consistently (the old bug was per-webview shortcut registration) | |||
| 239 | - | for i := 0; i < 3; i++ { | |||
| 240 | - | // Move focus left | |||
| 241 | - | wm.currentlyFocused = left | |||
| 242 | - | wm.focusManager.SetActivePane(left) | |||
| 243 | - | if wm.GetActiveNode() != left { | |||
| 244 | - | t.Fatalf("expected left pane to be active after focusNode call %d", i) | |||
| 245 | - | } | |||
| 246 | - | ||||
| 247 | - | // Test navigation still works | |||
| 248 | - | if !wm.FocusNeighbor("right") { | |||
| 249 | - | t.Fatalf("expected navigation right to work after focus change %d", i) | |||
| 250 | - | } | |||
| 251 | - | ||||
| 252 | - | // Move focus right | |||
| 253 | - | wm.currentlyFocused = right | |||
| 254 | - | wm.focusManager.SetActivePane(right) | |||
| 255 | - | if wm.GetActiveNode() != right { | |||
| 256 | - | t.Fatalf("expected right pane to be active after focusNode call %d", i) | |||
| 257 | - | } | |||
| 258 | - | ||||
| 259 | - | // Test navigation still works | |||
| 260 | - | if !wm.FocusNeighbor("left") { | |||
| 261 | - | t.Fatalf("expected navigation left to work after focus change %d", i) | |||
| 262 | - | } | |||
| 263 | - | } | |||
| 264 | - | ||||
| 265 | - | // Final verification that we can navigate in both directions | |||
| 266 | - | if !wm.FocusNeighbor("right") { | |||
| 267 | - | t.Fatalf("expected final navigation right to work") | |||
| 268 | - | } | |||
| 269 | - | ||||
| 270 | - | if wm.GetActiveNode() != right { | |||
| 271 | - | t.Fatalf("expected final active pane to be right") | |||
| 272 | - | } | |||
| 273 | - | } | |||
D internal/app/browser/workspace_manager_tree_test.go +0 -933
| @@ -1,933 +0,0 @@ | |||
| 1 | - | package browser | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "testing" | |
| 6 | - | "time" | |
| 7 | - | ||
| 8 | - | "github.com/bnema/dumber/pkg/webkit" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | // Test helper to create mock pane node | |
| 12 | - | func 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 | |
| 23 | - | func 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 | |
| 62 | - | func 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 | - | ||
| 74 | - | func 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 | - | ||
| 124 | - | func 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 | - | ||
| 174 | - | func 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 | - | ||
| 224 | - | func 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 | - | ||
| 284 | - | func 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 | - | ||
| 311 | - | func 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 | - | ||
| 341 | - | func 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 | - | ||
| 389 | - | func 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 | - | ||
| 444 | - | func 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.currentlyFocused = 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.currentlyFocused != 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.currentlyFocused = 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.currentlyFocused != 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 | |
| 490 | - | func 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 | - | ||
| 624 | - | func 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 | - | ||
| 654 | - | func 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 | - | ||
| 842 | - | func 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 | |
| 876 | - | func 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.currentlyFocused = root | |
| 929 | - | wm.mainPane = root | |
| 930 | - | wm.viewToNode[rootView] = root | |
| 931 | - | ||
| 932 | - | return wm | |
| 933 | - | } | |
| @@ -1,933 +0,0 @@ | |||||
| 1 | - | package browser | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "fmt" | |||
| 5 | - | "testing" | |||
| 6 | - | "time" | |||
| 7 | - | ||||
| 8 | - | "github.com/bnema/dumber/pkg/webkit" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | // Test helper to create mock pane node | |||
| 12 | - | func 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 | |||
| 23 | - | func 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 | |||
| 62 | - | func 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 | - | ||||
| 74 | - | func 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 | - | ||||
| 124 | - | func 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 | - | ||||
| 174 | - | func 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 | - | ||||
| 224 | - | func 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 | - | ||||
| 284 | - | func 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 | - | ||||
| 311 | - | func 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 | - | ||||
| 341 | - | func 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 | - | ||||
| 389 | - | func 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 | - | ||||
| 444 | - | func 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.currentlyFocused = 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.currentlyFocused != 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.currentlyFocused = 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.currentlyFocused != 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 | |||
| 490 | - | func 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 | - | ||||
| 624 | - | func 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 | - | ||||
| 654 | - | func 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 | - | ||||
| 842 | - | func 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 | |||
| 876 | - | func 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.currentlyFocused = root | |||
| 929 | - | wm.mainPane = root | |||
| 930 | - | wm.viewToNode[rootView] = root | |||
| 931 | - | ||||
| 932 | - | return wm | |||
| 933 | - | } | |||