detached 333d85c

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-}
D internal/app/browser/workspace_test.go +0 -2211 large diff (2211 lines changed): collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close