1package content
2
3import (
4 "context"
5 "sync"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/dumber/internal/application/port"
12 "github.com/bnema/dumber/internal/domain/entity"
13)
14
15// ---------------------------------------------------------------------------
16// DetectPopupType
17// ---------------------------------------------------------------------------
18
19func TestDetectPopupType_BlankIsTab(t *testing.T) {
20 t.Parallel()
21
22 assert.Equal(t, PopupTypeTab, DetectPopupType("_blank"))
23}
24
25func TestDetectPopupType_EmptyIsPopup(t *testing.T) {
26 t.Parallel()
27
28 assert.Equal(t, PopupTypePopup, DetectPopupType(""))
29}
30
31func TestDetectPopupType_NamedFrameIsPopup(t *testing.T) {
32 t.Parallel()
33
34 assert.Equal(t, PopupTypePopup, DetectPopupType("myFrame"))
35}
36
37// ---------------------------------------------------------------------------
38// PopupType.String()
39// ---------------------------------------------------------------------------
40
41func TestPopupTypeString_Tab(t *testing.T) {
42 t.Parallel()
43
44 assert.Equal(t, "tab", PopupTypeTab.String())
45}
46
47func TestPopupTypeString_Popup(t *testing.T) {
48 t.Parallel()
49
50 assert.Equal(t, "popup", PopupTypePopup.String())
51}
52
53func TestPopupTypeString_Unknown(t *testing.T) {
54 t.Parallel()
55
56 assert.Equal(t, "unknown", PopupType(99).String())
57}
58
59// ---------------------------------------------------------------------------
60// GetBehavior
61// ---------------------------------------------------------------------------
62
63func TestGetBehavior_NilConfigReturnsSplit(t *testing.T) {
64 t.Parallel()
65
66 assert.Equal(t, entity.PopupBehaviorSplit, GetBehavior(PopupTypeTab, nil))
67 assert.Equal(t, entity.PopupBehaviorSplit, GetBehavior(PopupTypePopup, nil))
68}
69
70func TestGetBehavior_TabPopup_DefaultConfig(t *testing.T) {
71 t.Parallel()
72
73 // BlankTargetBehavior is empty → falls through to default "stacked"
74 cfg := &entity.PopupBehaviorConfig{}
75 assert.Equal(t, entity.PopupBehaviorStacked, GetBehavior(PopupTypeTab, cfg))
76}
77
78func TestGetBehavior_TabPopup_SplitConfig(t *testing.T) {
79 t.Parallel()
80
81 cfg := &entity.PopupBehaviorConfig{BlankTargetBehavior: "split"}
82 assert.Equal(t, entity.PopupBehaviorSplit, GetBehavior(PopupTypeTab, cfg))
83}
84
85func TestGetBehavior_TabPopup_StackedConfig(t *testing.T) {
86 t.Parallel()
87
88 cfg := &entity.PopupBehaviorConfig{BlankTargetBehavior: "stacked"}
89 assert.Equal(t, entity.PopupBehaviorStacked, GetBehavior(PopupTypeTab, cfg))
90}
91
92func TestGetBehavior_TabPopup_TabbedConfig(t *testing.T) {
93 t.Parallel()
94
95 cfg := &entity.PopupBehaviorConfig{BlankTargetBehavior: "tabbed"}
96 assert.Equal(t, entity.PopupBehaviorTabbed, GetBehavior(PopupTypeTab, cfg))
97}
98
99func TestGetBehavior_JSPopup_UsesBehaviorField(t *testing.T) {
100 t.Parallel()
101
102 cfg := &entity.PopupBehaviorConfig{Behavior: entity.PopupBehaviorWindowed}
103 assert.Equal(t, entity.PopupBehaviorWindowed, GetBehavior(PopupTypePopup, cfg))
104}
105
106func TestGetBehavior_JSPopup_DefaultConfig(t *testing.T) {
107 t.Parallel()
108
109 // Behavior zero-value is empty string; GetBehavior returns it as-is.
110 cfg := &entity.PopupBehaviorConfig{}
111 assert.Equal(t, entity.PopupBehavior(""), GetBehavior(PopupTypePopup, cfg))
112}
113
114func TestGetBehavior_TabPopup_BlocksWhenOpenInNewPaneFalse(t *testing.T) {
115 t.Parallel()
116
117 // GetBehavior itself does not honor OpenInNewPane — that is enforced by
118 // handlePopupCreate. GetBehavior still returns the configured value.
119 cfg := &entity.PopupBehaviorConfig{
120 OpenInNewPane: false,
121 BlankTargetBehavior: "split",
122 }
123 assert.Equal(t, entity.PopupBehaviorSplit, GetBehavior(PopupTypeTab, cfg))
124}
125
126// ---------------------------------------------------------------------------
127// trackOAuthPopup / capturePopupOAuthState
128// ---------------------------------------------------------------------------
129
130func TestTrackOAuthPopup_StoresState(t *testing.T) {
131 t.Parallel()
132
133 c := &Coordinator{
134 webViews: make(map[entity.PaneID]port.WebView),
135 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
136 }
137
138 popupID := port.WebViewID(1)
139 parentPaneID := entity.PaneID("parent-pane")
140
141 c.trackOAuthPopup(popupID, parentPaneID)
142
143 c.popupMu.RLock()
144 state, ok := c.popupOAuth[popupID]
145 c.popupMu.RUnlock()
146
147 require.True(t, ok, "oauth state should be tracked after trackOAuthPopup")
148 require.NotNil(t, state)
149 assert.Equal(t, parentPaneID, state.ParentPaneID)
150 assert.False(t, state.Seen, "Seen should be false before capturing callback URI")
151}
152
153func TestCapturePopupOAuthState_SuccessCallback(t *testing.T) {
154 t.Parallel()
155
156 c := &Coordinator{
157 webViews: make(map[entity.PaneID]port.WebView),
158 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
159 }
160
161 popupID := port.WebViewID(2)
162 c.trackOAuthPopup(popupID, entity.PaneID("parent"))
163 c.capturePopupOAuthState(popupID, "https://app.example.com/callback?code=abc123")
164
165 c.popupMu.RLock()
166 state := c.popupOAuth[popupID]
167 c.popupMu.RUnlock()
168
169 require.NotNil(t, state)
170 assert.True(t, state.Seen)
171 assert.True(t, state.Success)
172 assert.False(t, state.Error)
173 assert.Equal(t, "https://app.example.com/callback?code=abc123", state.CallbackURI)
174}
175
176func TestCapturePopupOAuthState_ErrorCallback(t *testing.T) {
177 t.Parallel()
178
179 c := &Coordinator{
180 webViews: make(map[entity.PaneID]port.WebView),
181 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
182 }
183
184 popupID := port.WebViewID(3)
185 c.trackOAuthPopup(popupID, entity.PaneID("parent"))
186 c.capturePopupOAuthState(popupID, "https://app.example.com/callback?error=access_denied")
187
188 c.popupMu.RLock()
189 state := c.popupOAuth[popupID]
190 c.popupMu.RUnlock()
191
192 require.NotNil(t, state)
193 assert.True(t, state.Seen)
194 assert.False(t, state.Success)
195 assert.True(t, state.Error)
196}
197
198func TestCapturePopupOAuthState_UnknownPopupIsNoop(t *testing.T) {
199 t.Parallel()
200
201 c := &Coordinator{
202 webViews: make(map[entity.PaneID]port.WebView),
203 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
204 }
205
206 // Should not panic when no tracking entry exists.
207 c.capturePopupOAuthState(port.WebViewID(999), "https://example.com/callback?code=x")
208
209 c.popupMu.RLock()
210 _, ok := c.popupOAuth[port.WebViewID(999)]
211 c.popupMu.RUnlock()
212
213 assert.False(t, ok)
214}
215
216// ---------------------------------------------------------------------------
217// SetPopupConfig
218// ---------------------------------------------------------------------------
219
220func TestSetPopupConfig_SetsFields(t *testing.T) {
221 t.Parallel()
222
223 c := &Coordinator{}
224
225 factory := &stubFactory{}
226 cfg := &entity.PopupBehaviorConfig{Behavior: entity.PopupBehaviorSplit}
227 genID := func() string { return "test-id" }
228
229 c.SetPopupConfig(factory, cfg, genID)
230
231 assert.Equal(t, factory, c.factory)
232 assert.Equal(t, cfg, c.popupConfig)
233 assert.NotNil(t, c.generateID)
234 assert.Equal(t, "test-id", c.generateID())
235}
236
237func TestSetPopupConfig_NilConfigAllowed(t *testing.T) {
238 t.Parallel()
239
240 c := &Coordinator{}
241 c.SetPopupConfig(nil, nil, nil)
242
243 assert.Nil(t, c.factory)
244 assert.Nil(t, c.popupConfig)
245 assert.Nil(t, c.generateID)
246}
247
248// stubFactory satisfies port.WebViewFactory without importing the mocks package.
249type stubFactory struct{}
250
251func (s *stubFactory) Create(_ context.Context) (port.WebView, error) {
252 return nil, nil
253}
254
255func (s *stubFactory) CreateRelated(_ context.Context, _ port.WebViewID) (port.WebView, error) {
256 return nil, nil
257}
258
259// ---------------------------------------------------------------------------
260// Concurrent access to pendingPopups
261// ---------------------------------------------------------------------------
262
263func TestPendingPopups_ConcurrentAccess(t *testing.T) {
264 t.Parallel()
265
266 c := &Coordinator{
267 pendingPopups: make(map[port.WebViewID]*PendingPopup),
268 }
269
270 const workers = 20
271 var wg sync.WaitGroup
272 wg.Add(workers * 2)
273
274 // Writers
275 for i := 0; i < workers; i++ {
276 go func(id int) {
277 defer wg.Done()
278 popupID := port.WebViewID(id)
279 c.popupMu.Lock()
280 c.pendingPopups[popupID] = &PendingPopup{
281 FrameName: "_blank",
282 PopupType: PopupTypeTab,
283 }
284 c.popupMu.Unlock()
285 }(i)
286 }
287
288 // Readers
289 for i := 0; i < workers; i++ {
290 go func(id int) {
291 defer wg.Done()
292 popupID := port.WebViewID(id)
293 c.popupMu.RLock()
294 _ = c.pendingPopups[popupID]
295 c.popupMu.RUnlock()
296 }(i)
297 }
298
299 wg.Wait()
300
301 c.popupMu.RLock()
302 count := len(c.pendingPopups)
303 c.popupMu.RUnlock()
304
305 assert.Equal(t, workers, count, "each writer uses a unique key, so all inserts should be present")
306}
307
308func TestPendingPopups_ConcurrentDeleteAndRead(t *testing.T) {
309 t.Parallel()
310
311 c := &Coordinator{
312 pendingPopups: make(map[port.WebViewID]*PendingPopup),
313 }
314
315 // Pre-populate
316 for i := 0; i < 10; i++ {
317 c.pendingPopups[port.WebViewID(i)] = &PendingPopup{PopupType: PopupTypePopup}
318 }
319
320 var wg sync.WaitGroup
321 wg.Add(20)
322
323 // Deleters
324 for i := 0; i < 10; i++ {
325 go func(id int) {
326 defer wg.Done()
327 c.popupMu.Lock()
328 delete(c.pendingPopups, port.WebViewID(id))
329 c.popupMu.Unlock()
330 }(i)
331 }
332
333 // Readers
334 for i := 0; i < 10; i++ {
335 go func(id int) {
336 defer wg.Done()
337 c.popupMu.RLock()
338 _ = c.pendingPopups[port.WebViewID(id)]
339 c.popupMu.RUnlock()
340 }(i)
341 }
342
343 wg.Wait()
344
345 c.popupMu.RLock()
346 defer c.popupMu.RUnlock()
347 assert.Empty(t, c.pendingPopups, "all preloaded popups should have been deleted")
348}