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}