v0.15.0 9006957

workspace_navigation_test.go RawHistoryFolder

1package browser
2
3import (
4	"testing"
5
6	"github.com/diamondburned/gotk4/pkg/gtk/v4"
7)
8
9// TestFocusNeighborVerticalSplit tests basic tree structure validation
10// Note: Full geometry-based navigation testing requires integration tests with real GTK widgets
11func TestFocusNeighborVerticalSplit(t *testing.T) {
12	t.Skip("Structural neighbor navigation requires real GTK widgets for geometry calculations")
13}
14
15// TestFocusNeighborHorizontalSplit tests basic tree structure validation
16// Note: Full geometry-based navigation testing requires integration tests with real GTK widgets
17func TestFocusNeighborHorizontalSplit(t *testing.T) {
18	t.Skip("Structural neighbor navigation requires real GTK widgets for geometry calculations")
19}
20
21// TestPaneNodeStructure verifies basic pane node relationships
22func TestPaneNodeStructure(t *testing.T) {
23	root := &paneNode{
24		orientation: gtk.OrientationVertical,
25		isLeaf:      false,
26	}
27
28	child1 := &paneNode{
29		parent: root,
30		isLeaf: true,
31	}
32
33	child2 := &paneNode{
34		parent: root,
35		isLeaf: true,
36	}
37
38	root.left = child1
39	root.right = child2
40
41	// Verify parent-child relationships
42	if child1.parent != root {
43		t.Errorf("child1.parent should be root")
44	}
45
46	if child2.parent != root {
47		t.Errorf("child2.parent should be root")
48	}
49
50	// Verify root children
51	if root.left != child1 {
52		t.Errorf("root.left should be child1")
53	}
54
55	if root.right != child2 {
56		t.Errorf("root.right should be child2")
57	}
58
59	// Verify leaf status
60	if !child1.isLeaf {
61		t.Errorf("child1 should be a leaf")
62	}
63
64	if !child2.isLeaf {
65		t.Errorf("child2 should be a leaf")
66	}
67
68	if root.isLeaf {
69		t.Errorf("root should not be a leaf")
70	}
71}
72
73// TestMapDirection verifies direction mapping to GTK orientation
74func TestMapDirection(t *testing.T) {
75	tests := []struct {
76		direction     string
77		orientation   gtk.Orientation
78		existingFirst bool
79	}{
80		{DirectionDown, gtk.OrientationVertical, true},
81		{DirectionUp, gtk.OrientationVertical, false},
82		{DirectionRight, gtk.OrientationHorizontal, true},
83		{DirectionLeft, gtk.OrientationHorizontal, false},
84	}
85
86	for _, tt := range tests {
87		t.Run(tt.direction, func(t *testing.T) {
88			orientation, existingFirst := mapDirection(tt.direction)
89
90			if orientation != tt.orientation {
91				t.Errorf("mapDirection(%q) orientation = %v, want %v",
92					tt.direction, orientation, tt.orientation)
93			}
94
95			if existingFirst != tt.existingFirst {
96				t.Errorf("mapDirection(%q) existingFirst = %v, want %v",
97					tt.direction, existingFirst, tt.existingFirst)
98			}
99		})
100	}
101}
102
103// TestInvalidDirections verifies handling of invalid directions
104func TestInvalidDirections(t *testing.T) {
105	wm := &WorkspaceManager{}
106
107	invalidDirections := []string{
108		"",
109		"unknown",
110		"diagonal",
111	}
112
113	for _, direction := range invalidDirections {
114		t.Run(direction, func(t *testing.T) {
115			success := wm.FocusNeighbor(direction)
116			if success {
117				t.Errorf("Expected FocusNeighbor(%q) to return false, got true", direction)
118			}
119		})
120	}
121}
122
123// TestCaseSensitiveDirections verifies that direction strings are case-insensitive
124func TestCaseSensitiveDirections(t *testing.T) {
125	wm := &WorkspaceManager{}
126
127	// These should be accepted (case insensitive), but will return false
128	// because there's no active pane to navigate from
129	validDirections := []string{
130		"UP",
131		"Down",
132		"LEFT",
133		"Right",
134		DirectionUp,
135		DirectionDown,
136		DirectionLeft,
137		DirectionRight,
138	}
139
140	for _, direction := range validDirections {
141		t.Run(direction, func(t *testing.T) {
142			// Should not panic or error, just return false since no panes exist
143			_ = wm.FocusNeighbor(direction)
144		})
145	}
146}
147
148// TestNavigationCoordinateBug documents the coordinate space bug in navigation
149//
150// Bug Description:
151// When comparing positions for geometric navigation, the code mixes two coordinate spaces:
152// - Normal panes: return window-absolute coordinates from WidgetGetAllocation()
153// - Stacked panes: return stack-box-relative coordinates from WidgetGetAllocation()
154//
155// This causes navigation to fail because we're comparing coordinates in different spaces.
156//
157// Real-world example from logs:
158//
159//	Normal pane (top half):  center=(957, 532) - window coords
160//	Stack pane (bottom):     center=(955, 511) - relative to stack box!
161//	Result: dy=-21 → SKIPPED as "not below"
162//
163// The fix: Always use stack wrapper allocation for panes inside stacks.
164func TestNavigationCoordinateBug(t *testing.T) {
165	// Simulate the coordinate math from real user scenario
166	// Window: 1914x1064, vertical split: normal pane (top) + stack (bottom)
167
168	t.Run("BugValidation_CoordinateMixing", func(t *testing.T) {
169		// Normal pane occupies top half - window-absolute coords
170		normalCX, normalCY := 957.0, 266.0
171
172		// Stack pane child returns RELATIVE coordinates (bug!)
173		// Its allocation is relative to stack box at y=532
174		stackChildCX, stackChildCY := 957.0, 133.0 // Relative to parent!
175
176		dy := stackChildCY - normalCY
177		t.Logf("Normal pane: center=(%.0f, %.0f)", normalCX, normalCY)
178		t.Logf("Stack child (buggy relative): center=(%.0f, %.0f)", stackChildCX, stackChildCY)
179		t.Logf("dy = %.0f (negative = appears above)", dy)
180
181		// Navigation DOWN requires dy > 0, but we get negative!
182		const focusEpsilon = 0.5
183		if dy <= focusEpsilon {
184			t.Logf("✓ BUG: dy=%.0f, navigation DOWN would FAIL", dy)
185		}
186	})
187
188	t.Run("CorrectBehavior_UseStackWrapper", func(t *testing.T) {
189		// Normal pane (same as before)
190		normalCX, normalCY := 957.0, 266.0
191
192		// Stack wrapper has correct window-absolute coordinates
193		stackWrapperCX, stackWrapperCY := 957.0, 798.0
194
195		dy := stackWrapperCY - normalCY
196		t.Logf("Normal pane: center=(%.0f, %.0f)", normalCX, normalCY)
197		t.Logf("Stack wrapper (correct): center=(%.0f, %.0f)", stackWrapperCX, stackWrapperCY)
198		t.Logf("dy = %.0f (positive = correctly below)", dy)
199
200		const focusEpsilon = 0.5
201		if dy > focusEpsilon {
202			t.Logf("✓ CORRECT: Navigation DOWN would SUCCEED")
203		}
204
205		// Also verify UP navigation
206		dyUp := normalCY - stackWrapperCY
207		if dyUp < -focusEpsilon {
208			t.Logf("✓ CORRECT: Navigation UP would SUCCEED")
209		}
210	})
211}
212
213// TestGetNavigationAllocation tests the proposed fix helper function
214// This will be uncommented and updated after implementing the helper
215func TestGetNavigationAllocation(t *testing.T) {
216	t.Skip("Enable this test after implementing getNavigationAllocation() helper")
217
218	// TODO: After implementing getNavigationAllocation(), test:
219	// 1. Regular leaf pane → returns pane.container allocation
220	// 2. Pane inside stack → returns stack.stackWrapper allocation
221	// 3. Stack container itself → returns stack.stackWrapper allocation
222	// 4. Nil safety: node=nil, container=nil, parent=nil, stackWrapper=nil
223}

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