v0.28.0 3cb3770

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

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