detached 7484cef

test(coordinator): add pending reveal mechanism tests

commit
7484cef712d9cb188527c3017f099f97cf5bc3a4
author
bnema · · 2025-12-28 12:12 UTC
parent
05fc8f4
signature
GPG Unverified2B82207D370127A0 The key is unknown
1 file · +179 -0
A internal/ui/coordinator/content_reveal_test.go +179 -0
@@ -0,0 +1,179 @@
1+package coordinator
2+
3+import (
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/infrastructure/webkit"
12+)
13+
14+// TestPendingReveal tests the pending reveal state management.
15+// These are internal package tests that verify the state transitions
16+// without needing to mock WebKit dependencies.
17+
18+func TestMarkPendingReveal_SetsState(t *testing.T) {
19+ c := &ContentCoordinator{
20+ pendingReveal: make(map[entity.PaneID]bool),
21+ }
22+ paneID := entity.PaneID("pane-1")
23+
24+ c.markPendingReveal(paneID)
25+
26+ c.revealMu.Lock()
27+ defer c.revealMu.Unlock()
28+ assert.True(t, c.pendingReveal[paneID])
29+}
30+
31+func TestClearPendingReveal_RemovesState(t *testing.T) {
32+ c := &ContentCoordinator{
33+ pendingReveal: make(map[entity.PaneID]bool),
34+ }
35+ paneID := entity.PaneID("pane-1")
36+
37+ // First mark as pending
38+ c.markPendingReveal(paneID)
39+
40+ // Then clear
41+ c.clearPendingReveal(paneID)
42+
43+ c.revealMu.Lock()
44+ defer c.revealMu.Unlock()
45+ assert.False(t, c.pendingReveal[paneID])
46+}
47+
48+func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) {
49+ callbackCalled := false
50+ c := &ContentCoordinator{
51+ pendingReveal: make(map[entity.PaneID]bool),
52+ webViews: make(map[entity.PaneID]*webkit.WebView),
53+ onWebViewShown: func(paneID entity.PaneID) {
54+ callbackCalled = true
55+ },
56+ }
57+ paneID := entity.PaneID("pane-1")
58+
59+ // Call reveal without marking pending first
60+ c.revealIfPending(context.Background(), paneID, "http://example.com", "test")
61+
62+ assert.False(t, callbackCalled, "callback should not be called when not pending")
63+}
64+
65+func TestRevealIfPending_ClearsStateOnReveal(t *testing.T) {
66+ c := &ContentCoordinator{
67+ pendingReveal: make(map[entity.PaneID]bool),
68+ webViews: make(map[entity.PaneID]*webkit.WebView),
69+ }
70+ paneID := entity.PaneID("pane-1")
71+
72+ // Mark as pending
73+ c.markPendingReveal(paneID)
74+
75+ // Reveal (will return early since webview is nil, but should still clear state)
76+ c.revealIfPending(context.Background(), paneID, "http://example.com", "test")
77+
78+ c.revealMu.Lock()
79+ defer c.revealMu.Unlock()
80+ assert.False(t, c.pendingReveal[paneID], "pending state should be cleared after reveal attempt")
81+}
82+
83+func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) {
84+ revealCount := 0
85+ c := &ContentCoordinator{
86+ pendingReveal: make(map[entity.PaneID]bool),
87+ webViews: make(map[entity.PaneID]*webkit.WebView),
88+ onWebViewShown: func(paneID entity.PaneID) {
89+ revealCount++
90+ },
91+ }
92+ paneID := entity.PaneID("pane-1")
93+
94+ // Mark as pending
95+ c.markPendingReveal(paneID)
96+
97+ // Call reveal multiple times
98+ c.revealIfPending(context.Background(), paneID, "", "first")
99+ c.revealIfPending(context.Background(), paneID, "", "second")
100+ c.revealIfPending(context.Background(), paneID, "", "third")
101+
102+ // Callback should not be called since webview is nil, but state should be cleared
103+ assert.Equal(t, 0, revealCount, "callback not called when webview is nil")
104+
105+ // Verify state is cleared
106+ c.revealMu.Lock()
107+ defer c.revealMu.Unlock()
108+ assert.False(t, c.pendingReveal[paneID])
109+}
110+
111+func TestPendingReveal_ConcurrentAccess(t *testing.T) {
112+ c := &ContentCoordinator{
113+ pendingReveal: make(map[entity.PaneID]bool),
114+ webViews: make(map[entity.PaneID]*webkit.WebView),
115+ }
116+
117+ var wg sync.WaitGroup
118+ paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3", "pane-4", "pane-5"}
119+
120+ // Concurrently mark and clear panes
121+ for i := 0; i < 100; i++ {
122+ for _, paneID := range paneIDs {
123+ wg.Add(3)
124+
125+ go func(id entity.PaneID) {
126+ defer wg.Done()
127+ c.markPendingReveal(id)
128+ }(paneID)
129+
130+ go func(id entity.PaneID) {
131+ defer wg.Done()
132+ c.clearPendingReveal(id)
133+ }(paneID)
134+
135+ go func(id entity.PaneID) {
136+ defer wg.Done()
137+ c.revealIfPending(context.Background(), id, "", "concurrent")
138+ }(paneID)
139+ }
140+ }
141+
142+ wg.Wait()
143+ // Test passes if no race conditions occur
144+}
145+
146+func TestMarkPendingReveal_MultiplePanes(t *testing.T) {
147+ c := &ContentCoordinator{
148+ pendingReveal: make(map[entity.PaneID]bool),
149+ }
150+
151+ pane1 := entity.PaneID("pane-1")
152+ pane2 := entity.PaneID("pane-2")
153+
154+ c.markPendingReveal(pane1)
155+ c.markPendingReveal(pane2)
156+
157+ c.revealMu.Lock()
158+ defer c.revealMu.Unlock()
159+ assert.True(t, c.pendingReveal[pane1])
160+ assert.True(t, c.pendingReveal[pane2])
161+}
162+
163+func TestClearPendingReveal_OnlyAffectsTargetPane(t *testing.T) {
164+ c := &ContentCoordinator{
165+ pendingReveal: make(map[entity.PaneID]bool),
166+ }
167+
168+ pane1 := entity.PaneID("pane-1")
169+ pane2 := entity.PaneID("pane-2")
170+
171+ c.markPendingReveal(pane1)
172+ c.markPendingReveal(pane2)
173+ c.clearPendingReveal(pane1)
174+
175+ c.revealMu.Lock()
176+ defer c.revealMu.Unlock()
177+ assert.False(t, c.pendingReveal[pane1], "pane1 should be cleared")
178+ assert.True(t, c.pendingReveal[pane2], "pane2 should still be pending")
179+}
@@ -0,0 +1,179 @@
1+package coordinator
2+
3+import (
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/infrastructure/webkit"
12+)
13+
14+// TestPendingReveal tests the pending reveal state management.
15+// These are internal package tests that verify the state transitions
16+// without needing to mock WebKit dependencies.
17+
18+func TestMarkPendingReveal_SetsState(t *testing.T) {
19+ c := &ContentCoordinator{
20+ pendingReveal: make(map[entity.PaneID]bool),
21+ }
22+ paneID := entity.PaneID("pane-1")
23+
24+ c.markPendingReveal(paneID)
25+
26+ c.revealMu.Lock()
27+ defer c.revealMu.Unlock()
28+ assert.True(t, c.pendingReveal[paneID])
29+}
30+
31+func TestClearPendingReveal_RemovesState(t *testing.T) {
32+ c := &ContentCoordinator{
33+ pendingReveal: make(map[entity.PaneID]bool),
34+ }
35+ paneID := entity.PaneID("pane-1")
36+
37+ // First mark as pending
38+ c.markPendingReveal(paneID)
39+
40+ // Then clear
41+ c.clearPendingReveal(paneID)
42+
43+ c.revealMu.Lock()
44+ defer c.revealMu.Unlock()
45+ assert.False(t, c.pendingReveal[paneID])
46+}
47+
48+func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) {
49+ callbackCalled := false
50+ c := &ContentCoordinator{
51+ pendingReveal: make(map[entity.PaneID]bool),
52+ webViews: make(map[entity.PaneID]*webkit.WebView),
53+ onWebViewShown: func(paneID entity.PaneID) {
54+ callbackCalled = true
55+ },
56+ }
57+ paneID := entity.PaneID("pane-1")
58+
59+ // Call reveal without marking pending first
60+ c.revealIfPending(context.Background(), paneID, "http://example.com", "test")
61+
62+ assert.False(t, callbackCalled, "callback should not be called when not pending")
63+}
64+
65+func TestRevealIfPending_ClearsStateOnReveal(t *testing.T) {
66+ c := &ContentCoordinator{
67+ pendingReveal: make(map[entity.PaneID]bool),
68+ webViews: make(map[entity.PaneID]*webkit.WebView),
69+ }
70+ paneID := entity.PaneID("pane-1")
71+
72+ // Mark as pending
73+ c.markPendingReveal(paneID)
74+
75+ // Reveal (will return early since webview is nil, but should still clear state)
76+ c.revealIfPending(context.Background(), paneID, "http://example.com", "test")
77+
78+ c.revealMu.Lock()
79+ defer c.revealMu.Unlock()
80+ assert.False(t, c.pendingReveal[paneID], "pending state should be cleared after reveal attempt")
81+}
82+
83+func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) {
84+ revealCount := 0
85+ c := &ContentCoordinator{
86+ pendingReveal: make(map[entity.PaneID]bool),
87+ webViews: make(map[entity.PaneID]*webkit.WebView),
88+ onWebViewShown: func(paneID entity.PaneID) {
89+ revealCount++
90+ },
91+ }
92+ paneID := entity.PaneID("pane-1")
93+
94+ // Mark as pending
95+ c.markPendingReveal(paneID)
96+
97+ // Call reveal multiple times
98+ c.revealIfPending(context.Background(), paneID, "", "first")
99+ c.revealIfPending(context.Background(), paneID, "", "second")
100+ c.revealIfPending(context.Background(), paneID, "", "third")
101+
102+ // Callback should not be called since webview is nil, but state should be cleared
103+ assert.Equal(t, 0, revealCount, "callback not called when webview is nil")
104+
105+ // Verify state is cleared
106+ c.revealMu.Lock()
107+ defer c.revealMu.Unlock()
108+ assert.False(t, c.pendingReveal[paneID])
109+}
110+
111+func TestPendingReveal_ConcurrentAccess(t *testing.T) {
112+ c := &ContentCoordinator{
113+ pendingReveal: make(map[entity.PaneID]bool),
114+ webViews: make(map[entity.PaneID]*webkit.WebView),
115+ }
116+
117+ var wg sync.WaitGroup
118+ paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3", "pane-4", "pane-5"}
119+
120+ // Concurrently mark and clear panes
121+ for i := 0; i < 100; i++ {
122+ for _, paneID := range paneIDs {
123+ wg.Add(3)
124+
125+ go func(id entity.PaneID) {
126+ defer wg.Done()
127+ c.markPendingReveal(id)
128+ }(paneID)
129+
130+ go func(id entity.PaneID) {
131+ defer wg.Done()
132+ c.clearPendingReveal(id)
133+ }(paneID)
134+
135+ go func(id entity.PaneID) {
136+ defer wg.Done()
137+ c.revealIfPending(context.Background(), id, "", "concurrent")
138+ }(paneID)
139+ }
140+ }
141+
142+ wg.Wait()
143+ // Test passes if no race conditions occur
144+}
145+
146+func TestMarkPendingReveal_MultiplePanes(t *testing.T) {
147+ c := &ContentCoordinator{
148+ pendingReveal: make(map[entity.PaneID]bool),
149+ }
150+
151+ pane1 := entity.PaneID("pane-1")
152+ pane2 := entity.PaneID("pane-2")
153+
154+ c.markPendingReveal(pane1)
155+ c.markPendingReveal(pane2)
156+
157+ c.revealMu.Lock()
158+ defer c.revealMu.Unlock()
159+ assert.True(t, c.pendingReveal[pane1])
160+ assert.True(t, c.pendingReveal[pane2])
161+}
162+
163+func TestClearPendingReveal_OnlyAffectsTargetPane(t *testing.T) {
164+ c := &ContentCoordinator{
165+ pendingReveal: make(map[entity.PaneID]bool),
166+ }
167+
168+ pane1 := entity.PaneID("pane-1")
169+ pane2 := entity.PaneID("pane-2")
170+
171+ c.markPendingReveal(pane1)
172+ c.markPendingReveal(pane2)
173+ c.clearPendingReveal(pane1)
174+
175+ c.revealMu.Lock()
176+ defer c.revealMu.Unlock()
177+ assert.False(t, c.pendingReveal[pane1], "pane1 should be cleared")
178+ assert.True(t, c.pendingReveal[pane2], "pane2 should still be pending")
179+}

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