v0.33.1 9577fee

manager_test.go RawHistoryFolder

1package focus
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/assert"
7
8	"github.com/bnema/dumber/internal/application/usecase"
9	"github.com/bnema/dumber/internal/domain/entity"
10)
11
12// TestNavigateWithinStack_UsesActiveStackIndex tests that navigation uses
13// ActiveStackIndex from the domain model instead of pointer comparison.
14// Note: navigateWithinStack is a pure function that returns the target pane ID
15// without modifying state. The actual state mutation happens in ApplyFocusChange.
16func TestNavigateWithinStack_UsesActiveStackIndex(t *testing.T) {
17	// Create a stack with 3 children
18	pane0 := &entity.Pane{ID: "pane0"}
19	pane1 := &entity.Pane{ID: "pane1"}
20	pane2 := &entity.Pane{ID: "pane2"}
21
22	child0 := &entity.PaneNode{ID: "child0", Pane: pane0}
23	child1 := &entity.PaneNode{ID: "child1", Pane: pane1}
24	child2 := &entity.PaneNode{ID: "child2", Pane: pane2}
25
26	stackNode := &entity.PaneNode{
27		ID:               "stack",
28		IsStacked:        true,
29		Children:         []*entity.PaneNode{child0, child1, child2},
30		ActiveStackIndex: 1, // Currently at middle pane
31	}
32
33	// Set parents
34	child0.Parent = stackNode
35	child1.Parent = stackNode
36	child2.Parent = stackNode
37
38	m := NewManager(nil)
39
40	tests := []struct {
41		name        string
42		activeIndex int
43		direction   usecase.NavigateDirection
44		wantCanNav  bool
45		wantPaneID  entity.PaneID
46	}{
47		{
48			name:        "navigate down from middle",
49			activeIndex: 1,
50			direction:   usecase.NavDown,
51			wantCanNav:  true,
52			wantPaneID:  "pane2",
53		},
54		{
55			name:        "navigate up from middle",
56			activeIndex: 1,
57			direction:   usecase.NavUp,
58			wantCanNav:  true,
59			wantPaneID:  "pane0",
60		},
61		{
62			name:        "navigate down from last - boundary",
63			activeIndex: 2,
64			direction:   usecase.NavDown,
65			wantCanNav:  false,
66			wantPaneID:  "",
67		},
68		{
69			name:        "navigate up from first - boundary",
70			activeIndex: 0,
71			direction:   usecase.NavUp,
72			wantCanNav:  false,
73			wantPaneID:  "",
74		},
75		{
76			name:        "navigate down from first",
77			activeIndex: 0,
78			direction:   usecase.NavDown,
79			wantCanNav:  true,
80			wantPaneID:  "pane1",
81		},
82		{
83			name:        "navigate up from last",
84			activeIndex: 2,
85			direction:   usecase.NavUp,
86			wantCanNav:  true,
87			wantPaneID:  "pane1",
88		},
89	}
90
91	for _, tt := range tests {
92		t.Run(tt.name, func(t *testing.T) {
93			// Set up the active index
94			stackNode.ActiveStackIndex = tt.activeIndex
95
96			// navigateWithinStack is a pure function - it does not modify state
97			canNav, paneID := m.navigateWithinStack(stackNode, stackNode.Children[tt.activeIndex], tt.direction)
98
99			assert.Equal(t, tt.wantCanNav, canNav)
100			assert.Equal(t, tt.wantPaneID, paneID)
101		})
102	}
103}
104
105// TestNavigateWithinStack_EmptyStack tests navigation in empty stack returns false.
106func TestNavigateWithinStack_EmptyStack(t *testing.T) {
107	stackNode := &entity.PaneNode{
108		ID:        "stack",
109		IsStacked: true,
110		Children:  []*entity.PaneNode{},
111	}
112
113	m := NewManager(nil)
114
115	canNav, paneID := m.navigateWithinStack(stackNode, nil, usecase.NavDown)
116
117	assert.False(t, canNav)
118	assert.Empty(t, paneID)
119}
120
121// TestNavigateWithinStack_NotStacked tests navigation on non-stacked node returns false.
122func TestNavigateWithinStack_NotStacked(t *testing.T) {
123	node := &entity.PaneNode{
124		ID:        "leaf",
125		IsStacked: false,
126	}
127
128	m := NewManager(nil)
129
130	canNav, paneID := m.navigateWithinStack(node, nil, usecase.NavDown)
131
132	assert.False(t, canNav)
133	assert.Empty(t, paneID)
134}
135
136// TestNavigateWithinStack_InvalidActiveIndex tests navigation with invalid ActiveStackIndex
137// falls back to pointer search.
138func TestNavigateWithinStack_InvalidActiveIndexFallback(t *testing.T) {
139	pane0 := &entity.Pane{ID: "pane0"}
140	pane1 := &entity.Pane{ID: "pane1"}
141
142	child0 := &entity.PaneNode{ID: "child0", Pane: pane0}
143	child1 := &entity.PaneNode{ID: "child1", Pane: pane1}
144
145	stackNode := &entity.PaneNode{
146		ID:               "stack",
147		IsStacked:        true,
148		Children:         []*entity.PaneNode{child0, child1},
149		ActiveStackIndex: -1, // Invalid index
150	}
151
152	child0.Parent = stackNode
153	child1.Parent = stackNode
154
155	m := NewManager(nil)
156
157	// With invalid ActiveStackIndex, should fall back to pointer search
158	// Pass child0 as current node
159	canNav, paneID := m.navigateWithinStack(stackNode, child0, usecase.NavDown)
160
161	assert.True(t, canNav)
162	assert.Equal(t, entity.PaneID("pane1"), paneID)
163}
164
165// TestNavigateWithinStack_InvalidDirection tests navigation with invalid direction returns false.
166func TestNavigateWithinStack_InvalidDirection(t *testing.T) {
167	pane0 := &entity.Pane{ID: "pane0"}
168	child0 := &entity.PaneNode{ID: "child0", Pane: pane0}
169
170	stackNode := &entity.PaneNode{
171		ID:               "stack",
172		IsStacked:        true,
173		Children:         []*entity.PaneNode{child0},
174		ActiveStackIndex: 0,
175	}
176
177	m := NewManager(nil)
178
179	// NavLeft and NavRight should not work for stack navigation
180	canNav, paneID := m.navigateWithinStack(stackNode, child0, usecase.NavLeft)
181
182	assert.False(t, canNav)
183	assert.Empty(t, paneID)
184}
185
186// TestNavigateWithinStack_ReturnsCorrectPaneID tests that navigation returns
187// the correct target pane ID for sequential navigation.
188// Note: navigateWithinStack is now a pure function - state updates happen in ApplyFocusChange.
189func TestNavigateWithinStack_ReturnsCorrectPaneID(t *testing.T) {
190	pane0 := &entity.Pane{ID: "pane0"}
191	pane1 := &entity.Pane{ID: "pane1"}
192	pane2 := &entity.Pane{ID: "pane2"}
193
194	child0 := &entity.PaneNode{ID: "child0", Pane: pane0}
195	child1 := &entity.PaneNode{ID: "child1", Pane: pane1}
196	child2 := &entity.PaneNode{ID: "child2", Pane: pane2}
197
198	stackNode := &entity.PaneNode{
199		ID:               "stack",
200		IsStacked:        true,
201		Children:         []*entity.PaneNode{child0, child1, child2},
202		ActiveStackIndex: 0,
203	}
204
205	m := NewManager(nil)
206
207	// Navigate down from first - should return pane1
208	stackNode.ActiveStackIndex = 0
209	canNav, paneID := m.navigateWithinStack(stackNode, child0, usecase.NavDown)
210	assert.True(t, canNav)
211	assert.Equal(t, entity.PaneID("pane1"), paneID)
212
213	// Navigate down from middle - should return pane2
214	stackNode.ActiveStackIndex = 1
215	canNav, paneID = m.navigateWithinStack(stackNode, child1, usecase.NavDown)
216	assert.True(t, canNav)
217	assert.Equal(t, entity.PaneID("pane2"), paneID)
218
219	// Navigate up from last - should return pane1
220	stackNode.ActiveStackIndex = 2
221	canNav, paneID = m.navigateWithinStack(stackNode, child2, usecase.NavUp)
222	assert.True(t, canNav)
223	assert.Equal(t, entity.PaneID("pane1"), paneID)
224
225	// Navigate up from middle - should return pane0
226	stackNode.ActiveStackIndex = 1
227	canNav, paneID = m.navigateWithinStack(stackNode, child1, usecase.NavUp)
228	assert.True(t, canNav)
229	assert.Equal(t, entity.PaneID("pane0"), paneID)
230}

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