1package content
2
3import (
4 "context"
5 "sync"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9
10 "github.com/bnema/dumber/internal/application/port"
11 "github.com/bnema/dumber/internal/application/port/mocks"
12 "github.com/bnema/dumber/internal/domain/entity"
13 "github.com/bnema/dumber/internal/ui/component"
14)
15
16// TestPendingReveal tests the pending reveal state management.
17// These are internal package tests that verify the state transitions
18// without needing to mock WebKit dependencies.
19
20func TestMarkPendingReveal_SetsState(t *testing.T) {
21 c := &Coordinator{
22 pendingReveal: make(map[entity.PaneID]bool),
23 }
24 paneID := entity.PaneID("pane-1")
25
26 c.markPendingReveal(paneID)
27
28 c.revealMu.Lock()
29 defer c.revealMu.Unlock()
30 assert.True(t, c.pendingReveal[paneID])
31}
32
33func TestClearPendingReveal_RemovesState(t *testing.T) {
34 c := &Coordinator{
35 pendingReveal: make(map[entity.PaneID]bool),
36 }
37 paneID := entity.PaneID("pane-1")
38
39 // First mark as pending
40 c.markPendingReveal(paneID)
41
42 // Then clear
43 c.clearPendingReveal(paneID)
44
45 c.revealMu.Lock()
46 defer c.revealMu.Unlock()
47 assert.False(t, c.pendingReveal[paneID])
48}
49
50func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) {
51 callbackCalled := false
52 c := &Coordinator{
53 pendingReveal: make(map[entity.PaneID]bool),
54 webViews: make(map[entity.PaneID]port.WebView),
55 onWebViewShown: func(paneID entity.PaneID) {
56 callbackCalled = true
57 },
58 }
59 paneID := entity.PaneID("pane-1")
60
61 // Call reveal without marking pending first
62 c.revealIfPending(context.Background(), paneID, "http://example.com", "test")
63
64 assert.False(t, callbackCalled, "callback should not be called when not pending")
65}
66
67func TestRevealIfPending_ClearsStateOnReveal(t *testing.T) {
68 c := &Coordinator{
69 pendingReveal: make(map[entity.PaneID]bool),
70 webViews: make(map[entity.PaneID]port.WebView),
71 }
72 paneID := entity.PaneID("pane-1")
73
74 // Mark as pending
75 c.markPendingReveal(paneID)
76
77 // Reveal (will return early since webview is nil, but should still clear state)
78 c.revealIfPending(context.Background(), paneID, "http://example.com", "test")
79
80 c.revealMu.Lock()
81 defer c.revealMu.Unlock()
82 assert.False(t, c.pendingReveal[paneID], "pending state should be cleared after reveal attempt")
83}
84
85func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) {
86 revealCount := 0
87 c := &Coordinator{
88 pendingReveal: make(map[entity.PaneID]bool),
89 webViews: make(map[entity.PaneID]port.WebView),
90 onWebViewShown: func(paneID entity.PaneID) {
91 revealCount++
92 },
93 }
94 paneID := entity.PaneID("pane-1")
95
96 // Mark as pending
97 c.markPendingReveal(paneID)
98
99 // Call reveal multiple times
100 c.revealIfPending(context.Background(), paneID, "", "first")
101 c.revealIfPending(context.Background(), paneID, "", "second")
102 c.revealIfPending(context.Background(), paneID, "", "third")
103
104 // Callback should not be called since webview is nil, but state should be cleared
105 assert.Equal(t, 0, revealCount, "callback not called when webview is nil")
106
107 // Verify state is cleared
108 c.revealMu.Lock()
109 defer c.revealMu.Unlock()
110 assert.False(t, c.pendingReveal[paneID])
111}
112
113func TestOnLoadCommitted_RevealsPendingWebViewForCommittedPage(t *testing.T) {
114 paneID := entity.PaneID("pane-1")
115 shownCount := 0
116 var shownPane entity.PaneID
117
118 wv := mocks.NewMockWebView(t)
119 wv.EXPECT().URI().Return("https://example.com")
120 wv.EXPECT().ResetBackgroundToDefault()
121 wv.EXPECT().Title().Return("")
122 wv.EXPECT().IsDestroyed().Return(false).Maybe()
123
124 c := &Coordinator{
125 pendingReveal: make(map[entity.PaneID]bool),
126 webViews: map[entity.PaneID]port.WebView{
127 paneID: wv,
128 },
129 paneTitles: make(map[entity.PaneID]string),
130 navOrigins: make(map[entity.PaneID]string),
131 getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) { return nil, nil },
132 onWebViewShown: func(id entity.PaneID) {
133 shownCount++
134 shownPane = id
135 },
136 }
137 c.pendingReveal[paneID] = true
138
139 c.onLoadCommitted(context.Background(), paneID, wv)
140
141 assert.Equal(t, 1, shownCount)
142 assert.Equal(t, paneID, shownPane)
143}
144
145func TestPendingReveal_ConcurrentAccess(t *testing.T) {
146 c := &Coordinator{
147 pendingReveal: make(map[entity.PaneID]bool),
148 webViews: make(map[entity.PaneID]port.WebView),
149 }
150
151 var wg sync.WaitGroup
152 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3", "pane-4", "pane-5"}
153
154 // Concurrently mark and clear panes
155 for i := 0; i < 100; i++ {
156 for _, paneID := range paneIDs {
157 wg.Add(3)
158
159 go func(id entity.PaneID) {
160 defer wg.Done()
161 c.markPendingReveal(id)
162 }(paneID)
163
164 go func(id entity.PaneID) {
165 defer wg.Done()
166 c.clearPendingReveal(id)
167 }(paneID)
168
169 go func(id entity.PaneID) {
170 defer wg.Done()
171 c.revealIfPending(context.Background(), id, "", "concurrent")
172 }(paneID)
173 }
174 }
175
176 wg.Wait()
177 // Test passes if no race conditions occur
178}
179
180func TestMarkPendingReveal_MultiplePanes(t *testing.T) {
181 c := &Coordinator{
182 pendingReveal: make(map[entity.PaneID]bool),
183 }
184
185 pane1 := entity.PaneID("pane-1")
186 pane2 := entity.PaneID("pane-2")
187
188 c.markPendingReveal(pane1)
189 c.markPendingReveal(pane2)
190
191 c.revealMu.Lock()
192 defer c.revealMu.Unlock()
193 assert.True(t, c.pendingReveal[pane1])
194 assert.True(t, c.pendingReveal[pane2])
195}
196
197func TestClearPendingReveal_OnlyAffectsTargetPane(t *testing.T) {
198 c := &Coordinator{
199 pendingReveal: make(map[entity.PaneID]bool),
200 }
201
202 pane1 := entity.PaneID("pane-1")
203 pane2 := entity.PaneID("pane-2")
204
205 c.markPendingReveal(pane1)
206 c.markPendingReveal(pane2)
207 c.clearPendingReveal(pane1)
208
209 c.revealMu.Lock()
210 defer c.revealMu.Unlock()
211 assert.False(t, c.pendingReveal[pane1], "pane1 should be cleared")
212 assert.True(t, c.pendingReveal[pane2], "pane2 should still be pending")
213}