v0.30.0 eb3068f

title_test.go RawHistoryFolder

1package content
2
3import (
4	"context"
5	"sync"
6	"testing"
7
8	"github.com/stretchr/testify/assert"
9
10	"github.com/bnema/dumber/internal/domain/entity"
11	"github.com/bnema/dumber/internal/ui/component"
12)
13
14func TestOnTitleChanged_DoesNotEmitWindowCallbackForBackgroundPane(t *testing.T) {
15	t.Parallel()
16
17	pane1 := entity.PaneID("pane-1")
18	pane2 := entity.PaneID("pane-2")
19	root := &entity.PaneNode{ID: "root"}
20	leaf1 := &entity.PaneNode{ID: string(pane1), Pane: entity.NewPane(pane1), Parent: root}
21	leaf2 := &entity.PaneNode{ID: string(pane2), Pane: entity.NewPane(pane2), Parent: root}
22	root.Children = []*entity.PaneNode{leaf1, leaf2}
23
24	ws := &entity.Workspace{Root: root, ActivePaneID: pane1}
25	var gotPaneID entity.PaneID
26	var gotTitle string
27	c := &Coordinator{
28		paneTitles:  make(map[entity.PaneID]string),
29		getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) { return ws, nil },
30		onWindowTitleChanged: func(paneID entity.PaneID, title string) {
31			gotPaneID = paneID
32			gotTitle = title
33		},
34	}
35
36	c.onTitleChanged(context.Background(), pane2, "Pane Two")
37
38	assert.Empty(t, gotPaneID)
39	assert.Empty(t, gotTitle)
40	assert.Equal(t, "Pane Two", c.GetTitle(pane2))
41}
42
43func TestOnTitleChanged_EmitsWindowCallbackForActivePane(t *testing.T) {
44	t.Parallel()
45
46	pane := entity.PaneID("pane-1")
47	root := &entity.PaneNode{ID: "root"}
48	leaf := &entity.PaneNode{ID: string(pane), Pane: entity.NewPane(pane), Parent: root}
49	root.Children = []*entity.PaneNode{leaf}
50
51	ws := &entity.Workspace{Root: root, ActivePaneID: pane}
52	var gotPaneID entity.PaneID
53	var gotTitle string
54	c := &Coordinator{
55		paneTitles:  make(map[entity.PaneID]string),
56		getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) { return ws, nil },
57		onWindowTitleChanged: func(paneID entity.PaneID, title string) {
58			gotPaneID = paneID
59			gotTitle = title
60		},
61	}
62
63	c.onTitleChanged(context.Background(), pane, "Pane One")
64
65	assert.Equal(t, pane, gotPaneID)
66	assert.Equal(t, "Pane One", gotTitle)
67	assert.Equal(t, "Pane One", c.GetTitle(pane))
68}
69
70// TestGetTitle tests the GetTitle method for title state retrieval.
71
72func TestGetTitle_ReturnsEmptyForUnknownPane(t *testing.T) {
73	t.Parallel()
74
75	c := &Coordinator{
76		paneTitles: make(map[entity.PaneID]string),
77	}
78
79	title := c.GetTitle("unknown-pane")
80	assert.Empty(t, title)
81}
82
83func TestGetTitle_ReturnsTrackedTitle(t *testing.T) {
84	t.Parallel()
85
86	paneID := entity.PaneID("pane-1")
87	c := &Coordinator{
88		paneTitles: map[entity.PaneID]string{
89			paneID: "My Page Title",
90		},
91	}
92
93	title := c.GetTitle(paneID)
94	assert.Equal(t, "My Page Title", title)
95}
96
97func TestGetTitle_DirectMapUpdate(t *testing.T) {
98	t.Parallel()
99
100	paneID := entity.PaneID("pane-1")
101	c := &Coordinator{
102		paneTitles: make(map[entity.PaneID]string),
103	}
104
105	// Simulate the write that onTitleChanged performs
106	c.titleMu.Lock()
107	c.paneTitles[paneID] = "Updated Title"
108	c.titleMu.Unlock()
109
110	title := c.GetTitle(paneID)
111	assert.Equal(t, "Updated Title", title)
112}
113
114func TestGetTitle_EmptyStringAfterZeroValue(t *testing.T) {
115	t.Parallel()
116
117	paneID := entity.PaneID("pane-1")
118	c := &Coordinator{
119		paneTitles: map[entity.PaneID]string{
120			paneID: "",
121		},
122	}
123
124	title := c.GetTitle(paneID)
125	assert.Empty(t, title)
126}
127
128// TestPaneTitles_ConcurrentAccess verifies that concurrent reads and writes to
129// paneTitles do not race.
130func TestPaneTitles_ConcurrentAccess(t *testing.T) {
131	t.Parallel()
132
133	c := &Coordinator{
134		paneTitles: make(map[entity.PaneID]string),
135	}
136
137	paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3"}
138
139	var wg sync.WaitGroup
140	for i := 0; i < 50; i++ {
141		for _, paneID := range paneIDs {
142			wg.Add(2)
143
144			go func(id entity.PaneID) {
145				defer wg.Done()
146				c.titleMu.Lock()
147				c.paneTitles[id] = "title for " + string(id)
148				c.titleMu.Unlock()
149			}(paneID)
150
151			go func(id entity.PaneID) {
152				defer wg.Done()
153				_ = c.GetTitle(id)
154			}(paneID)
155		}
156	}
157
158	wg.Wait()
159	// Test passes if no race conditions occur
160}
161
162func TestPaneTitles_MultiplepanesTrackedIndependently(t *testing.T) {
163	t.Parallel()
164
165	pane1 := entity.PaneID("pane-1")
166	pane2 := entity.PaneID("pane-2")
167
168	c := &Coordinator{
169		paneTitles: map[entity.PaneID]string{
170			pane1: "Title One",
171			pane2: "Title Two",
172		},
173	}
174
175	assert.Equal(t, "Title One", c.GetTitle(pane1))
176	assert.Equal(t, "Title Two", c.GetTitle(pane2))
177}
178
179// TestSetNavigationOrigin tests navigation origin storage and retrieval.
180
181func TestSetNavigationOrigin_StoresOriginURL(t *testing.T) {
182	t.Parallel()
183
184	paneID := entity.PaneID("pane-1")
185	c := &Coordinator{
186		navOrigins: make(map[entity.PaneID]string),
187	}
188
189	c.SetNavigationOrigin(paneID, "https://google.fr/search?q=test")
190
191	c.navOriginMu.RLock()
192	origin := c.navOrigins[paneID]
193	c.navOriginMu.RUnlock()
194
195	assert.Equal(t, "https://google.fr/search?q=test", origin)
196}
197
198func TestSetNavigationOrigin_OverwritesPreviousOrigin(t *testing.T) {
199	t.Parallel()
200
201	paneID := entity.PaneID("pane-1")
202	c := &Coordinator{
203		navOrigins: make(map[entity.PaneID]string),
204	}
205
206	c.SetNavigationOrigin(paneID, "https://google.fr/")
207	c.SetNavigationOrigin(paneID, "https://example.com/new")
208
209	c.navOriginMu.RLock()
210	origin := c.navOrigins[paneID]
211	c.navOriginMu.RUnlock()
212
213	assert.Equal(t, "https://example.com/new", origin)
214}
215
216func TestSetNavigationOrigin_IndependentPerPane(t *testing.T) {
217	t.Parallel()
218
219	pane1 := entity.PaneID("pane-1")
220	pane2 := entity.PaneID("pane-2")
221	c := &Coordinator{
222		navOrigins: make(map[entity.PaneID]string),
223	}
224
225	c.SetNavigationOrigin(pane1, "https://pane1.example.com/")
226	c.SetNavigationOrigin(pane2, "https://pane2.example.com/")
227
228	c.navOriginMu.RLock()
229	origin1 := c.navOrigins[pane1]
230	origin2 := c.navOrigins[pane2]
231	c.navOriginMu.RUnlock()
232
233	assert.Equal(t, "https://pane1.example.com/", origin1)
234	assert.Equal(t, "https://pane2.example.com/", origin2)
235}
236
237func TestSetNavigationOrigin_EmptyOrigin(t *testing.T) {
238	t.Parallel()
239
240	paneID := entity.PaneID("pane-1")
241	c := &Coordinator{
242		navOrigins: map[entity.PaneID]string{
243			paneID: "https://old-origin.example.com/",
244		},
245	}
246
247	c.SetNavigationOrigin(paneID, "")
248
249	c.navOriginMu.RLock()
250	origin := c.navOrigins[paneID]
251	c.navOriginMu.RUnlock()
252
253	assert.Empty(t, origin)
254}
255
256// TestNavOrigins_ConcurrentAccess verifies that concurrent reads and writes to
257// navOrigins do not race.
258func TestNavOrigins_ConcurrentAccess(t *testing.T) {
259	t.Parallel()
260
261	c := &Coordinator{
262		navOrigins: make(map[entity.PaneID]string),
263	}
264
265	paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3"}
266
267	var wg sync.WaitGroup
268	for i := 0; i < 50; i++ {
269		for _, paneID := range paneIDs {
270			wg.Add(2)
271
272			go func(id entity.PaneID) {
273				defer wg.Done()
274				c.SetNavigationOrigin(id, "https://origin-for-"+string(id)+".example.com/")
275			}(paneID)
276
277			go func(id entity.PaneID) {
278				defer wg.Done()
279				c.navOriginMu.RLock()
280				_ = c.navOrigins[id]
281				c.navOriginMu.RUnlock()
282			}(paneID)
283		}
284	}
285
286	wg.Wait()
287	// Test passes if no race conditions occur
288}
289
290// TestQueueThemeApply tests the appearance queue management for theme applies.
291
292func TestQueueThemeApply_TakeReturnsTrue(t *testing.T) {
293	t.Parallel()
294
295	paneID := entity.PaneID("pane-1")
296	c := &Coordinator{}
297
298	c.queueThemeApply(paneID, true, "body { color: white; }")
299
300	update, ok := c.takePendingThemeApply(paneID)
301	assert.True(t, ok)
302	assert.True(t, update.prefersDark)
303	assert.Equal(t, "body { color: white; }", update.cssText)
304}
305
306func TestQueueThemeApply_SecondTakeReturnsFalse(t *testing.T) {
307	t.Parallel()
308
309	paneID := entity.PaneID("pane-1")
310	c := &Coordinator{}
311
312	c.queueThemeApply(paneID, false, "body { color: black; }")
313
314	_, firstOk := c.takePendingThemeApply(paneID)
315	assert.True(t, firstOk)
316
317	_, secondOk := c.takePendingThemeApply(paneID)
318	assert.False(t, secondOk)
319}
320
321func TestQueueThemeApply_TakeWithoutQueue_ReturnsFalse(t *testing.T) {
322	t.Parallel()
323
324	paneID := entity.PaneID("pane-1")
325	c := &Coordinator{}
326
327	_, ok := c.takePendingThemeApply(paneID)
328	assert.False(t, ok)
329}
330
331func TestQueueThemeApply_MultiplePanesQueued_Independently(t *testing.T) {
332	t.Parallel()
333
334	pane1 := entity.PaneID("pane-1")
335	pane2 := entity.PaneID("pane-2")
336	c := &Coordinator{}
337
338	// pendingThemeUpdate is a single shared payload; queueThemeApply always
339	// overwrites it with the latest call. Both panes are marked as pending,
340	// but they share the last-written update.
341	c.queueThemeApply(pane1, true, "shared-css")
342	c.queueThemeApply(pane2, true, "shared-css")
343
344	// Both panes should be independently takeable
345	update1, ok1 := c.takePendingThemeApply(pane1)
346	assert.True(t, ok1)
347	assert.Equal(t, "shared-css", update1.cssText)
348
349	// pane2 should still be queued after taking pane1
350	update2, ok2 := c.takePendingThemeApply(pane2)
351	assert.True(t, ok2)
352	assert.Equal(t, "shared-css", update2.cssText)
353
354	// Neither should be available after both are taken
355	_, ok3 := c.takePendingThemeApply(pane1)
356	assert.False(t, ok3)
357	_, ok4 := c.takePendingThemeApply(pane2)
358	assert.False(t, ok4)
359}
360
361func TestQueueThemeApply_LastWriteWins(t *testing.T) {
362	t.Parallel()
363
364	paneID := entity.PaneID("pane-1")
365	c := &Coordinator{}
366
367	// Queue twice; the second call overwrites pendingThemeUpdate
368	c.queueThemeApply(paneID, true, "first-css")
369	c.queueThemeApply(paneID, false, "second-css")
370
371	update, ok := c.takePendingThemeApply(paneID)
372	assert.True(t, ok)
373	assert.False(t, update.prefersDark)
374	assert.Equal(t, "second-css", update.cssText)
375}
376
377func TestQueueThemeApply_HasPendingFlagClearedWhenAllTaken(t *testing.T) {
378	t.Parallel()
379
380	pane1 := entity.PaneID("pane-1")
381	pane2 := entity.PaneID("pane-2")
382	c := &Coordinator{}
383
384	c.queueThemeApply(pane1, true, "css")
385	c.queueThemeApply(pane2, true, "css")
386
387	_, _ = c.takePendingThemeApply(pane1)
388
389	// hasPendingThemeUpdate should still be true since pane2 is pending
390	c.appearanceMu.Lock()
391	assert.True(t, c.hasPendingThemeUpdate)
392	c.appearanceMu.Unlock()
393
394	_, _ = c.takePendingThemeApply(pane2)
395
396	// Now hasPendingThemeUpdate should be false
397	c.appearanceMu.Lock()
398	assert.False(t, c.hasPendingThemeUpdate)
399	c.appearanceMu.Unlock()
400}
401
402// TestQueueScriptRefresh tests the appearance queue management for script refreshes.
403
404func TestQueueScriptRefresh_TakeReturnsTrue(t *testing.T) {
405	t.Parallel()
406
407	paneID := entity.PaneID("pane-1")
408	c := &Coordinator{}
409
410	c.queueScriptRefresh(paneID)
411
412	ok := c.takePendingScriptRefresh(paneID)
413	assert.True(t, ok)
414}
415
416func TestQueueScriptRefresh_SecondTakeReturnsFalse(t *testing.T) {
417	t.Parallel()
418
419	paneID := entity.PaneID("pane-1")
420	c := &Coordinator{}
421
422	c.queueScriptRefresh(paneID)
423
424	firstOk := c.takePendingScriptRefresh(paneID)
425	assert.True(t, firstOk)
426
427	secondOk := c.takePendingScriptRefresh(paneID)
428	assert.False(t, secondOk)
429}
430
431func TestQueueScriptRefresh_TakeWithoutQueue_ReturnsFalse(t *testing.T) {
432	t.Parallel()
433
434	paneID := entity.PaneID("pane-1")
435	c := &Coordinator{}
436
437	ok := c.takePendingScriptRefresh(paneID)
438	assert.False(t, ok)
439}
440
441func TestQueueScriptRefresh_MultiplePanesQueuedIndependently(t *testing.T) {
442	t.Parallel()
443
444	pane1 := entity.PaneID("pane-1")
445	pane2 := entity.PaneID("pane-2")
446	pane3 := entity.PaneID("pane-3")
447	c := &Coordinator{}
448
449	c.queueScriptRefresh(pane1)
450	c.queueScriptRefresh(pane2)
451
452	// pane3 was never queued
453	assert.True(t, c.takePendingScriptRefresh(pane1))
454	assert.True(t, c.takePendingScriptRefresh(pane2))
455	assert.False(t, c.takePendingScriptRefresh(pane3))
456}
457
458// TestApplyPendingThemeUpdate tests the state management aspect of applyPendingThemeUpdate.
459
460func TestApplyPendingThemeUpdate_ReturnsFalseWhenNothingQueued(t *testing.T) {
461	t.Parallel()
462
463	paneID := entity.PaneID("pane-1")
464	c := &Coordinator{}
465
466	// applyPendingThemeUpdate calls takePendingThemeApply; with nothing queued it
467	// returns false immediately without touching the WebView.
468	result := c.applyPendingThemeUpdate(context.TODO(), paneID, nil)
469	assert.False(t, result)
470}
471
472// TestClearPendingAppearance tests that clearPendingAppearance removes both
473// theme and script queues for the targeted pane only.
474func TestClearPendingAppearance_ClearsBothQueuesForPane(t *testing.T) {
475	t.Parallel()
476
477	pane1 := entity.PaneID("pane-1")
478	pane2 := entity.PaneID("pane-2")
479	c := &Coordinator{}
480
481	c.queueThemeApply(pane1, true, "css")
482	c.queueScriptRefresh(pane1)
483	c.queueThemeApply(pane2, true, "css")
484	c.queueScriptRefresh(pane2)
485
486	c.clearPendingAppearance(pane1)
487
488	// pane1 queues should be gone
489	assert.False(t, c.takePendingScriptRefresh(pane1))
490	_, themeOk1 := c.takePendingThemeApply(pane1)
491	assert.False(t, themeOk1)
492
493	// pane2 queues should still be present
494	assert.True(t, c.takePendingScriptRefresh(pane2))
495	_, themeOk2 := c.takePendingThemeApply(pane2)
496	assert.True(t, themeOk2)
497}

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