v0.26.1 75cd777

session_state_test.go RawHistoryFolder

1package entity_test
2
3import (
4	"sync/atomic"
5	"testing"
6	"time"
7
8	"github.com/bnema/dumber/internal/domain/entity"
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11)
12
13// mockIDGenerator creates a simple ID generator for testing.
14func mockIDGenerator() entity.IDGenerator {
15	var counter uint64
16	return func() string {
17		id := atomic.AddUint64(&counter, 1)
18		return string(rune('a'-1+id)) + "_new"
19	}
20}
21
22func TestTabListFromSnapshot_EmptyState(t *testing.T) {
23	idGen := mockIDGenerator()
24
25	// Nil state
26	tabs := entity.TabListFromSnapshot(nil, idGen)
27	require.NotNil(t, tabs)
28	assert.Empty(t, tabs.Tabs)
29
30	// Empty state
31	state := &entity.SessionState{
32		Version: 1,
33		Tabs:    []entity.TabSnapshot{},
34	}
35	tabs = entity.TabListFromSnapshot(state, idGen)
36	require.NotNil(t, tabs)
37	assert.Empty(t, tabs.Tabs)
38}
39
40func TestTabListFromSnapshot_SingleTab(t *testing.T) {
41	idGen := mockIDGenerator()
42
43	state := &entity.SessionState{
44		Version:        1,
45		SessionID:      "test_session",
46		ActiveTabIndex: 0,
47		Tabs: []entity.TabSnapshot{
48			{
49				ID:       "old_tab_1",
50				Name:     "Test Tab",
51				Position: 0,
52				IsPinned: true,
53				Workspace: entity.WorkspaceSnapshot{
54					ID:           "old_ws_1",
55					ActivePaneID: "old_pane_1",
56					Root: &entity.PaneNodeSnapshot{
57						ID: "old_node_1",
58						Pane: &entity.PaneSnapshot{
59							ID:         "old_pane_1",
60							URI:        "https://example.com",
61							Title:      "Example",
62							ZoomFactor: 1.25,
63						},
64					},
65				},
66			},
67		},
68		SavedAt: time.Now(),
69	}
70
71	tabs := entity.TabListFromSnapshot(state, idGen)
72
73	require.NotNil(t, tabs)
74	require.Len(t, tabs.Tabs, 1)
75
76	tab := tabs.Tabs[0]
77	assert.NotEqual(t, entity.TabID("old_tab_1"), tab.ID, "should generate new ID")
78	assert.Equal(t, "Test Tab", tab.Name)
79	assert.True(t, tab.IsPinned)
80
81	require.NotNil(t, tab.Workspace)
82	require.NotNil(t, tab.Workspace.Root)
83	require.NotNil(t, tab.Workspace.Root.Pane)
84
85	pane := tab.Workspace.Root.Pane
86	assert.NotEqual(t, entity.PaneID("old_pane_1"), pane.ID, "should generate new pane ID")
87	assert.Equal(t, "https://example.com", pane.URI)
88	assert.Equal(t, "Example", pane.Title)
89	assert.InDelta(t, 1.25, pane.ZoomFactor, 0.001)
90
91	// Active tab should be set
92	assert.Equal(t, tab.ID, tabs.ActiveTabID)
93}
94
95func TestTabListFromSnapshot_MultipleTabs(t *testing.T) {
96	idGen := mockIDGenerator()
97
98	state := &entity.SessionState{
99		Version:        1,
100		ActiveTabIndex: 1, // Second tab is active
101		Tabs: []entity.TabSnapshot{
102			{
103				ID:   "tab_1",
104				Name: "First",
105				Workspace: entity.WorkspaceSnapshot{
106					Root: &entity.PaneNodeSnapshot{
107						Pane: &entity.PaneSnapshot{
108							ID:  "pane_1",
109							URI: "https://first.com",
110						},
111					},
112				},
113			},
114			{
115				ID:   "tab_2",
116				Name: "Second",
117				Workspace: entity.WorkspaceSnapshot{
118					Root: &entity.PaneNodeSnapshot{
119						Pane: &entity.PaneSnapshot{
120							ID:  "pane_2",
121							URI: "https://second.com",
122						},
123					},
124				},
125			},
126		},
127	}
128
129	tabs := entity.TabListFromSnapshot(state, idGen)
130
131	require.Len(t, tabs.Tabs, 2)
132	assert.Equal(t, "First", tabs.Tabs[0].Name)
133	assert.Equal(t, "Second", tabs.Tabs[1].Name)
134
135	// Active tab should be the second one
136	assert.Equal(t, tabs.Tabs[1].ID, tabs.ActiveTabID)
137}
138
139func TestTabListFromSnapshot_SplitPanes(t *testing.T) {
140	idGen := mockIDGenerator()
141
142	// Horizontal split with two panes
143	state := &entity.SessionState{
144		Version: 1,
145		Tabs: []entity.TabSnapshot{
146			{
147				ID:   "tab_1",
148				Name: "Split Tab",
149				Workspace: entity.WorkspaceSnapshot{
150					Root: &entity.PaneNodeSnapshot{
151						ID:         "container",
152						SplitDir:   entity.SplitHorizontal,
153						SplitRatio: 0.5,
154						Children: []*entity.PaneNodeSnapshot{
155							{
156								ID: "left_node",
157								Pane: &entity.PaneSnapshot{
158									ID:  "left_pane",
159									URI: "https://left.com",
160								},
161							},
162							{
163								ID: "right_node",
164								Pane: &entity.PaneSnapshot{
165									ID:  "right_pane",
166									URI: "https://right.com",
167								},
168							},
169						},
170					},
171				},
172			},
173		},
174	}
175
176	tabs := entity.TabListFromSnapshot(state, idGen)
177
178	require.Len(t, tabs.Tabs, 1)
179	ws := tabs.Tabs[0].Workspace
180	require.NotNil(t, ws.Root)
181
182	// Root should be a container with children
183	assert.Nil(t, ws.Root.Pane)
184	assert.Equal(t, entity.SplitHorizontal, ws.Root.SplitDir)
185	assert.InDelta(t, 0.5, ws.Root.SplitRatio, 0.001)
186	require.Len(t, ws.Root.Children, 2)
187
188	// Check children
189	leftPane := ws.Root.Children[0].Pane
190	rightPane := ws.Root.Children[1].Pane
191	require.NotNil(t, leftPane)
192	require.NotNil(t, rightPane)
193	assert.Equal(t, "https://left.com", leftPane.URI)
194	assert.Equal(t, "https://right.com", rightPane.URI)
195}
196
197func TestTabListFromSnapshot_StackedPanes(t *testing.T) {
198	idGen := mockIDGenerator()
199
200	state := &entity.SessionState{
201		Version: 1,
202		Tabs: []entity.TabSnapshot{
203			{
204				ID: "tab_1",
205				Workspace: entity.WorkspaceSnapshot{
206					Root: &entity.PaneNodeSnapshot{
207						ID:               "stack",
208						IsStacked:        true,
209						ActiveStackIndex: 1,
210						Children: []*entity.PaneNodeSnapshot{
211							{Pane: &entity.PaneSnapshot{ID: "p1", URI: "https://a.com"}},
212							{Pane: &entity.PaneSnapshot{ID: "p2", URI: "https://b.com"}},
213							{Pane: &entity.PaneSnapshot{ID: "p3", URI: "https://c.com"}},
214						},
215					},
216				},
217			},
218		},
219	}
220
221	tabs := entity.TabListFromSnapshot(state, idGen)
222
223	ws := tabs.Tabs[0].Workspace
224	require.NotNil(t, ws.Root)
225	assert.True(t, ws.Root.IsStacked)
226	assert.Equal(t, 1, ws.Root.ActiveStackIndex)
227	require.Len(t, ws.Root.Children, 3)
228}
229
230func TestTabList_ReplaceFrom(t *testing.T) {
231	// Create original list
232	original := entity.NewTabList()
233	pane1 := entity.NewPane("pane1")
234	pane1.URI = "https://old.com"
235	tab1 := entity.NewTab("tab1", "ws1", pane1)
236	original.Add(tab1)
237	original.SetActive("tab1")
238
239	// Create new list
240	newList := entity.NewTabList()
241	pane2 := entity.NewPane("pane2")
242	pane2.URI = "https://new1.com"
243	pane3 := entity.NewPane("pane3")
244	pane3.URI = "https://new2.com"
245	tab2 := entity.NewTab("tab2", "ws2", pane2)
246	tab3 := entity.NewTab("tab3", "ws3", pane3)
247	newList.Add(tab2)
248	newList.Add(tab3)
249	newList.SetActive("tab3")
250
251	// Keep reference to original
252	ref := original
253
254	// Replace contents
255	original.ReplaceFrom(newList)
256
257	// Verify the reference still points to the modified list
258	assert.Same(t, original, ref)
259	assert.Len(t, ref.Tabs, 2)
260	assert.Equal(t, entity.TabID("tab3"), ref.ActiveTabID)
261	assert.Equal(t, "https://new1.com", ref.Tabs[0].Workspace.Root.Pane.URI)
262}
263
264func TestTabList_ReplaceFrom_Nil(t *testing.T) {
265	original := entity.NewTabList()
266	pane := entity.NewPane("pane1")
267	tab := entity.NewTab("tab1", "ws1", pane)
268	original.Add(tab)
269
270	original.ReplaceFrom(nil)
271
272	assert.Empty(t, original.Tabs)
273	assert.Empty(t, original.ActiveTabID)
274}
275
276func TestSnapshotRoundTrip(t *testing.T) {
277	// Create a complex tab structure
278	idGen := mockIDGenerator()
279
280	pane1 := entity.NewPane("p1")
281	pane1.URI = "https://google.com"
282	pane1.Title = "Google"
283	pane1.ZoomFactor = 1.5
284
285	pane2 := entity.NewPane("p2")
286	pane2.URI = "https://github.com"
287	pane2.Title = "GitHub"
288
289	tab1 := entity.NewTab("t1", "ws1", pane1)
290	tab1.Name = "Search"
291	tab1.IsPinned = true
292
293	tab2 := entity.NewTab("t2", "ws2", pane2)
294	tab2.Name = "Code"
295
296	original := entity.NewTabList()
297	original.Add(tab1)
298	original.Add(tab2)
299	original.SetActive("t2")
300
301	// Snapshot
302	sessionID := entity.SessionID("test_session")
303	state := entity.SnapshotFromTabList(sessionID, original)
304
305	require.NotNil(t, state)
306	assert.Equal(t, sessionID, state.SessionID)
307	assert.Len(t, state.Tabs, 2)
308	assert.Equal(t, 1, state.ActiveTabIndex) // Second tab active
309
310	// Restore
311	restored := entity.TabListFromSnapshot(state, idGen)
312
313	require.NotNil(t, restored)
314	require.Len(t, restored.Tabs, 2)
315
316	// Check first tab
317	restoredTab1 := restored.Tabs[0]
318	assert.Equal(t, "Search", restoredTab1.Name)
319	assert.True(t, restoredTab1.IsPinned)
320	require.NotNil(t, restoredTab1.Workspace.Root.Pane)
321	assert.Equal(t, "https://google.com", restoredTab1.Workspace.Root.Pane.URI)
322	assert.Equal(t, "Google", restoredTab1.Workspace.Root.Pane.Title)
323	assert.InDelta(t, 1.5, restoredTab1.Workspace.Root.Pane.ZoomFactor, 0.001)
324
325	// Check second tab
326	restoredTab2 := restored.Tabs[1]
327	assert.Equal(t, "Code", restoredTab2.Name)
328	assert.False(t, restoredTab2.IsPinned)
329	assert.Equal(t, "https://github.com", restoredTab2.Workspace.Root.Pane.URI)
330
331	// Active tab should be second
332	assert.Equal(t, restoredTab2.ID, restored.ActiveTabID)
333}
334
335func TestFindPaneAcrossTabs(t *testing.T) {
336	// Create tabs with panes
337	pane1 := entity.NewPane("pane_in_tab1")
338	pane1.URI = "https://tab1.com"
339	tab1 := entity.NewTab("t1", "ws1", pane1)
340
341	pane2 := entity.NewPane("pane_in_tab2")
342	pane2.URI = "https://tab2.com"
343	tab2 := entity.NewTab("t2", "ws2", pane2)
344
345	tabs := entity.NewTabList()
346	tabs.Add(tab1)
347	tabs.Add(tab2)
348
349	// Helper function to find pane across all tabs (mirrors App.updatePaneURIInAllTabs logic)
350	findPaneInTabs := func(tabs *entity.TabList, paneID entity.PaneID) *entity.Pane {
351		for _, tab := range tabs.Tabs {
352			if tab.Workspace == nil {
353				continue
354			}
355			paneNode := tab.Workspace.FindPane(paneID)
356			if paneNode != nil && paneNode.Pane != nil {
357				return paneNode.Pane
358			}
359		}
360		return nil
361	}
362
363	// Find pane in first tab
364	found := findPaneInTabs(tabs, "pane_in_tab1")
365	require.NotNil(t, found)
366	assert.Equal(t, "https://tab1.com", found.URI)
367
368	// Find pane in second tab
369	found = findPaneInTabs(tabs, "pane_in_tab2")
370	require.NotNil(t, found)
371	assert.Equal(t, "https://tab2.com", found.URI)
372
373	// Update URI and verify
374	found.URI = "https://updated.com"
375	foundAgain := findPaneInTabs(tabs, "pane_in_tab2")
376	assert.Equal(t, "https://updated.com", foundAgain.URI)
377
378	// Non-existent pane
379	notFound := findPaneInTabs(tabs, "non_existent")
380	assert.Nil(t, notFound)
381}
382
383func TestFindPaneInNestedStructure(t *testing.T) {
384	// Create a tab with split panes
385	pane1 := entity.NewPane("left_pane")
386	pane1.URI = "https://left.com"
387
388	pane2 := entity.NewPane("right_pane")
389	pane2.URI = "https://right.com"
390
391	// Build a split structure manually
392	tab := entity.NewTab("t1", "ws1", pane1)
393
394	// Add second pane as sibling (simulating a horizontal split)
395	rightNode := &entity.PaneNode{
396		ID:   "right_node",
397		Pane: pane2,
398	}
399
400	// Create container node
401	containerNode := &entity.PaneNode{
402		ID:       "container",
403		SplitDir: entity.SplitHorizontal,
404		Children: []*entity.PaneNode{
405			tab.Workspace.Root, // left pane
406			rightNode,          // right pane
407		},
408	}
409	tab.Workspace.Root = containerNode
410
411	tabs := entity.NewTabList()
412	tabs.Add(tab)
413
414	// Find left pane
415	leftNode := tab.Workspace.FindPane("left_pane")
416	require.NotNil(t, leftNode)
417	assert.Equal(t, "https://left.com", leftNode.Pane.URI)
418
419	// Find right pane
420	rightNodeFound := tab.Workspace.FindPane("right_pane")
421	require.NotNil(t, rightNodeFound)
422	assert.Equal(t, "https://right.com", rightNodeFound.Pane.URI)
423
424	// Update URI on nested pane
425	rightNodeFound.Pane.URI = "https://right-updated.com"
426
427	// Verify update persisted
428	rightNodeAgain := tab.Workspace.FindPane("right_pane")
429	assert.Equal(t, "https://right-updated.com", rightNodeAgain.Pane.URI)
430}

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