1package cef
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 purecef "github.com/bnema/purego-cef/cef"
9 "github.com/stretchr/testify/require"
10)
11
12type viewportSyncOrderHost struct {
13 purecef.BrowserHost
14 calls []string
15 zoomLevel float64
16}
17
18type pendingBrowserCreateObservedSizeBridgeStub struct {
19 sizeWidth int32
20 sizeHeight int32
21 refreshWidth int32
22 refreshHeight int32
23 refreshCalls int
24 refreshProvided bool
25}
26
27func (s *pendingBrowserCreateObservedSizeBridgeStub) Size() (int32, int32) {
28 return s.sizeWidth, s.sizeHeight
29}
30
31func (s *pendingBrowserCreateObservedSizeBridgeStub) RefreshObservedSizeOnGTKThread() (int32, int32) {
32 s.refreshCalls++
33 if s.refreshProvided {
34 return s.refreshWidth, s.refreshHeight
35 }
36 return s.sizeWidth, s.sizeHeight
37}
38
39func (h *viewportSyncOrderHost) WasHidden(state int32) {
40 h.calls = append(h.calls, "WasHidden")
41 if state != 0 {
42 h.calls = append(h.calls, "WasHidden(nonzero)")
43 }
44}
45
46func (h *viewportSyncOrderHost) NotifyScreenInfoChanged() {
47 h.calls = append(h.calls, "NotifyScreenInfoChanged")
48}
49
50func (h *viewportSyncOrderHost) WasResized() {
51 h.calls = append(h.calls, "WasResized")
52}
53
54func (h *viewportSyncOrderHost) Invalidate(_ purecef.PaintElementType) {
55 h.calls = append(h.calls, "Invalidate")
56}
57
58func (h *viewportSyncOrderHost) SetZoomLevel(level float64) {
59 h.calls = append(h.calls, "SetZoomLevel")
60 h.zoomLevel = level
61}
62
63func (h *viewportSyncOrderHost) GetZoomLevel() float64 {
64 return h.zoomLevel
65}
66
67func TestNotifyBrowserViewportSync_VisibleCallsFullSequence(t *testing.T) {
68 host := &viewportSyncOrderHost{}
69
70 notifyBrowserViewportSync(host, true)
71
72 require.Equal(t, []string{"WasHidden", "NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)
73}
74
75func TestNotifyBrowserViewportSync_HiddenSkipsWasHidden(t *testing.T) {
76 host := &viewportSyncOrderHost{}
77
78 notifyBrowserViewportSync(host, false)
79
80 require.Equal(t, []string{"NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)
81}
82
83func TestNotifyViewportSyncOnCEFUIThread_PostsViewportWorkOnly(t *testing.T) {
84 oldNewTask := cefNewTask
85 oldPostDelayedTask := cefPostDelayedTask
86 defer func() {
87 cefNewTask = oldNewTask
88 cefPostDelayedTask = oldPostDelayedTask
89 }()
90
91 cefNewTask = func(task purecef.Task) purecef.Task { return task }
92
93 var scheduled purecef.Task
94 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
95 require.Equal(t, purecef.ThreadIDTidUi, threadID)
96 require.NotNil(t, task)
97 if delayMs == 0 && scheduled == nil {
98 scheduled = task
99 }
100 return 1
101 }
102
103 host := &viewportSyncOrderHost{}
104 wv := &WebView{ctx: context.Background(), host: host}
105 wv.notifyViewportSyncOnCEFUIThread(host, true)
106
107 require.Empty(t, host.calls)
108 require.NotNil(t, scheduled)
109
110 scheduled.Execute()
111 require.Equal(t, []string{
112 "WasHidden",
113 "NotifyScreenInfoChanged",
114 "WasResized",
115 "Invalidate",
116 }, host.calls)
117}
118
119func TestNotifyViewportSyncOnCEFUIThread_RemainsViewportOnlyAcrossRepeatedResizes(t *testing.T) {
120 oldNewTask := cefNewTask
121 oldPostDelayedTask := cefPostDelayedTask
122 defer func() {
123 cefNewTask = oldNewTask
124 cefPostDelayedTask = oldPostDelayedTask
125 }()
126
127 cefNewTask = func(task purecef.Task) purecef.Task { return task }
128
129 var scheduled purecef.Task
130 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
131 if delayMs == 0 && scheduled == nil {
132 scheduled = task
133 }
134 return 1
135 }
136
137 host := &viewportSyncOrderHost{}
138 wv := &WebView{ctx: context.Background(), host: host}
139
140 wv.notifyViewportSyncOnCEFUIThread(host, true)
141 require.NotNil(t, scheduled)
142 scheduled.Execute()
143
144 host.calls = nil
145 scheduled = nil
146
147 wv.notifyViewportSyncOnCEFUIThread(host, true)
148 require.NotNil(t, scheduled)
149 scheduled.Execute()
150
151 require.Equal(t, []string{
152 "NotifyScreenInfoChanged",
153 "WasResized",
154 "Invalidate",
155 }, host.calls)
156}
157
158func TestSyncZoomForBackingScaleOnCEFUIThread_PostsZoomWork(t *testing.T) {
159 oldNewTask := cefNewTask
160 oldPostDelayedTask := cefPostDelayedTask
161 defer func() {
162 cefNewTask = oldNewTask
163 cefPostDelayedTask = oldPostDelayedTask
164 }()
165
166 cefNewTask = func(task purecef.Task) purecef.Task { return task }
167
168 var scheduled purecef.Task
169 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
170 require.Equal(t, purecef.ThreadIDTidUi, threadID)
171 require.NotNil(t, task)
172 if delayMs == 0 && scheduled == nil {
173 scheduled = task
174 }
175 return 1
176 }
177
178 host := &viewportSyncOrderHost{}
179 wv := &WebView{ctx: context.Background(), host: host}
180 wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
181
182 require.Empty(t, host.calls)
183 require.NotNil(t, scheduled)
184
185 scheduled.Execute()
186 require.Equal(t, []string{
187 "SetZoomLevel",
188 "NotifyScreenInfoChanged",
189 }, host.calls)
190}
191
192func TestSyncZoomForBackingScaleOnCEFUIThread_SkipsRedundantReapplyWhenBackingScaleUnchanged(t *testing.T) {
193 oldNewTask := cefNewTask
194 oldPostDelayedTask := cefPostDelayedTask
195 defer func() {
196 cefNewTask = oldNewTask
197 cefPostDelayedTask = oldPostDelayedTask
198 }()
199
200 cefNewTask = func(task purecef.Task) purecef.Task { return task }
201
202 var scheduled purecef.Task
203 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
204 if delayMs == 0 && scheduled == nil {
205 scheduled = task
206 }
207 return 1
208 }
209
210 host := &viewportSyncOrderHost{}
211 wv := &WebView{ctx: context.Background(), host: host}
212
213 wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
214 require.NotNil(t, scheduled)
215 scheduled.Execute()
216
217 host.calls = nil
218 scheduled = nil
219
220 wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
221 require.Nil(t, scheduled)
222 require.Empty(t, host.calls)
223}
224
225func TestNotifyViewportSyncOnCEFUIThread_SkipsStaleHost(t *testing.T) {
226 oldNewTask := cefNewTask
227 oldPostDelayedTask := cefPostDelayedTask
228 defer func() {
229 cefNewTask = oldNewTask
230 cefPostDelayedTask = oldPostDelayedTask
231 }()
232
233 cefNewTask = func(task purecef.Task) purecef.Task { return task }
234
235 var scheduled purecef.Task
236 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
237 scheduled = task
238 return 1
239 }
240
241 capturedHost := &viewportSyncOrderHost{}
242 currentHost := &viewportSyncOrderHost{}
243 wv := &WebView{ctx: context.Background(), host: currentHost}
244 wv.notifyViewportSyncOnCEFUIThread(capturedHost, true)
245
246 require.NotNil(t, scheduled)
247 scheduled.Execute()
248 require.Empty(t, capturedHost.calls)
249 require.Empty(t, currentHost.calls)
250}
251
252func TestNotifyViewportSyncOnCEFUIThread_SkipsDestroyedWebView(t *testing.T) {
253 oldNewTask := cefNewTask
254 oldPostDelayedTask := cefPostDelayedTask
255 defer func() {
256 cefNewTask = oldNewTask
257 cefPostDelayedTask = oldPostDelayedTask
258 }()
259
260 cefNewTask = func(task purecef.Task) purecef.Task { return task }
261
262 var scheduled purecef.Task
263 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
264 scheduled = task
265 return 1
266 }
267
268 host := &viewportSyncOrderHost{}
269 wv := &WebView{ctx: context.Background(), host: host}
270 wv.destroyed.Store(true)
271 wv.notifyViewportSyncOnCEFUIThread(host, true)
272
273 require.NotNil(t, scheduled)
274 scheduled.Execute()
275 require.Empty(t, host.calls)
276}
277
278func TestObservedViewportSizeReady(t *testing.T) {
279 tests := []struct {
280 name string
281 observedWidth int32
282 observedHeight int32
283 allocatedWidth int32
284 allocatedHeight int32
285 want bool
286 }{
287 {
288 name: "exact match",
289 observedWidth: 793,
290 observedHeight: 1753,
291 allocatedWidth: 793,
292 allocatedHeight: 1753,
293 want: true,
294 },
295 {
296 name: "one pixel drift is accepted",
297 observedWidth: 792,
298 observedHeight: 1754,
299 allocatedWidth: 793,
300 allocatedHeight: 1753,
301 want: true,
302 },
303 {
304 name: "fallback observed size is rejected",
305 observedWidth: 1,
306 observedHeight: 1,
307 allocatedWidth: 793,
308 allocatedHeight: 1753,
309 want: false,
310 },
311 {
312 name: "missing allocation is rejected",
313 observedWidth: 793,
314 observedHeight: 1753,
315 allocatedWidth: 0,
316 allocatedHeight: 1753,
317 want: false,
318 },
319 {
320 name: "large mismatch is rejected",
321 observedWidth: 1200,
322 observedHeight: 1400,
323 allocatedWidth: 1587,
324 allocatedHeight: 1679,
325 want: false,
326 },
327 }
328
329 for _, tt := range tests {
330 t.Run(tt.name, func(t *testing.T) {
331 require.Equal(t, tt.want, observedViewportSizeReady(tt.observedWidth, tt.observedHeight, tt.allocatedWidth, tt.allocatedHeight))
332 })
333 }
334}
335
336func TestPendingBrowserCreateObservedSize_RefreshesStaleFallbackSize(t *testing.T) {
337 bridge := &pendingBrowserCreateObservedSizeBridgeStub{
338 sizeWidth: 1,
339 sizeHeight: 1,
340 refreshWidth: 1055,
341 refreshHeight: 1719,
342 refreshProvided: true,
343 }
344
345 observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
346
347 require.True(t, ready)
348 require.Equal(t, int32(1055), observedWidth)
349 require.Equal(t, int32(1719), observedHeight)
350 require.Equal(t, 1, bridge.refreshCalls)
351}
352
353func TestPendingBrowserCreateObservedSize_SkipsRefreshWhenObservedSizeAlreadyMatches(t *testing.T) {
354 bridge := &pendingBrowserCreateObservedSizeBridgeStub{sizeWidth: 1055, sizeHeight: 1719}
355
356 observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
357
358 require.True(t, ready)
359 require.Equal(t, int32(1055), observedWidth)
360 require.Equal(t, int32(1719), observedHeight)
361 require.Equal(t, 0, bridge.refreshCalls)
362}
363
364func TestShouldDeferPendingBrowserCreateFromViewportSync_WaitsForNativePopupAttach(t *testing.T) {
365 wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
366
367 require.True(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
368
369 wv.nativePopupFallbackStarted = true
370 require.False(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
371}
372
373func TestPreparePendingBrowserCreateObservedSizeRetry_CoalescesScheduling(t *testing.T) {
374 oldScheduleAfter := cefScheduleAfter
375 defer func() { cefScheduleAfter = oldScheduleAfter }()
376
377 scheduleCalls := 0
378 cefScheduleAfter = func(delay time.Duration, fn func()) {
379 require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
380 require.NotNil(t, fn)
381 scheduleCalls++
382 }
383
384 wv := &WebView{pendingCreate: &pendingBrowserCreate{}}
385
386 attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
387 require.Equal(t, 1, attempt)
388 require.Equal(t, pendingBrowserCreateObservedSizeRetryScheduled, action)
389 require.Equal(t, 1, scheduleCalls)
390
391 attempt, action = wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
392 require.Equal(t, 1, attempt)
393 require.Equal(t, pendingBrowserCreateObservedSizeRetryAlreadyScheduled, action)
394 require.Equal(t, 1, scheduleCalls)
395}
396
397func TestPreparePendingBrowserCreateObservedSizeRetry_MaxRetriesProceedsWithoutScheduling(t *testing.T) {
398 oldScheduleAfter := cefScheduleAfter
399 defer func() { cefScheduleAfter = oldScheduleAfter }()
400
401 scheduleCalls := 0
402 cefScheduleAfter = func(delay time.Duration, fn func()) {
403 require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
404 require.NotNil(t, fn)
405 scheduleCalls++
406 }
407
408 wv := &WebView{pendingCreate: &pendingBrowserCreate{observedSizeRetries: pendingBrowserCreateObservedSizeMaxRetries}}
409
410 attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
411 require.Equal(t, pendingBrowserCreateObservedSizeMaxRetries+1, attempt)
412 require.Equal(t, pendingBrowserCreateObservedSizeRetryProceedWithoutDelay, action)
413 require.Equal(t, 0, scheduleCalls)
414 require.False(t, wv.pendingCreate.observedSizeRetryScheduled)
415}
416
417func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
418 oldNewTask := cefNewTask
419 oldPostDelayedTask := cefPostDelayedTask
420 defer func() {
421 cefNewTask = oldNewTask
422 cefPostDelayedTask = oldPostDelayedTask
423 }()
424
425 cefNewTask = func(task purecef.Task) purecef.Task { return task }
426
427 var scheduled []purecef.Task
428 var delays []int64
429 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
430 require.Equal(t, purecef.ThreadIDTidUi, threadID)
431 require.NotNil(t, task)
432 scheduled = append(scheduled, task)
433 delays = append(delays, delayMs)
434 return 1
435 }
436
437 host := &viewportSyncOrderHost{}
438 wv := &WebView{ctx: context.Background(), host: host}
439
440 wv.scheduleResizeRepaintPulse(context.Background(), "first")
441 wv.scheduleResizeRepaintPulse(context.Background(), "second")
442
443 require.Equal(t, []int64{16, 48, 16, 48}, delays)
444 require.Len(t, scheduled, 4)
445
446 scheduled[0].Execute()
447 scheduled[1].Execute()
448 require.Empty(t, host.calls)
449
450 scheduled[2].Execute()
451 scheduled[3].Execute()
452 require.Equal(t, []string{
453 "NotifyScreenInfoChanged",
454 "WasResized",
455 "Invalidate",
456 "NotifyScreenInfoChanged",
457 "WasResized",
458 "Invalidate",
459 }, host.calls)
460}
461
462func TestScheduleResizeRepaintPulse_SkipsStaleHost(t *testing.T) {
463 oldNewTask := cefNewTask
464 oldPostDelayedTask := cefPostDelayedTask
465 defer func() {
466 cefNewTask = oldNewTask
467 cefPostDelayedTask = oldPostDelayedTask
468 }()
469
470 cefNewTask = func(task purecef.Task) purecef.Task { return task }
471
472 var scheduled []purecef.Task
473 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
474 require.Equal(t, purecef.ThreadIDTidUi, threadID)
475 require.NotNil(t, task)
476 scheduled = append(scheduled, task)
477 return 1
478 }
479
480 capturedHost := &viewportSyncOrderHost{}
481 currentHost := &viewportSyncOrderHost{}
482 wv := &WebView{ctx: context.Background(), host: capturedHost}
483
484 wv.scheduleResizeRepaintPulse(context.Background(), "stale-host")
485 wv.host = currentHost
486
487 require.Len(t, scheduled, 2)
488 scheduled[0].Execute()
489 scheduled[1].Execute()
490
491 require.Empty(t, capturedHost.calls)
492 require.Empty(t, currentHost.calls)
493}
494
495func TestScheduleResizeRepaintPulse_SkipsWhenHostNilAtScheduleTime(t *testing.T) {
496 oldNewTask := cefNewTask
497 oldPostDelayedTask := cefPostDelayedTask
498 defer func() {
499 cefNewTask = oldNewTask
500 cefPostDelayedTask = oldPostDelayedTask
501 }()
502
503 cefNewTask = func(task purecef.Task) purecef.Task { return task }
504
505 var scheduled []purecef.Task
506 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
507 scheduled = append(scheduled, task)
508 return 1
509 }
510
511 wv := &WebView{ctx: context.Background()}
512
513 wv.scheduleResizeRepaintPulse(context.Background(), "nil-host")
514
515 require.Empty(t, scheduled)
516}
517
518func TestScheduleResizeRepaintPulse_SkipsWhenHostClearedBeforeExecution(t *testing.T) {
519 oldNewTask := cefNewTask
520 oldPostDelayedTask := cefPostDelayedTask
521 defer func() {
522 cefNewTask = oldNewTask
523 cefPostDelayedTask = oldPostDelayedTask
524 }()
525
526 cefNewTask = func(task purecef.Task) purecef.Task { return task }
527
528 var scheduled []purecef.Task
529 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
530 require.Equal(t, purecef.ThreadIDTidUi, threadID)
531 require.NotNil(t, task)
532 scheduled = append(scheduled, task)
533 return 1
534 }
535
536 host := &viewportSyncOrderHost{}
537 wv := &WebView{ctx: context.Background(), host: host}
538
539 wv.scheduleResizeRepaintPulse(context.Background(), "nil-host-before-execute")
540 wv.host = nil
541
542 require.Len(t, scheduled, 2)
543 scheduled[0].Execute()
544 scheduled[1].Execute()
545
546 require.Empty(t, host.calls)
547}