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 "WasHidden",
153 "NotifyScreenInfoChanged",
154 "WasResized",
155 "Invalidate",
156 }, host.calls)
157}
158
159func TestSyncZoomForBackingScaleOnCEFUIThread_PostsZoomWork(t *testing.T) {
160 oldNewTask := cefNewTask
161 oldPostDelayedTask := cefPostDelayedTask
162 defer func() {
163 cefNewTask = oldNewTask
164 cefPostDelayedTask = oldPostDelayedTask
165 }()
166
167 cefNewTask = func(task purecef.Task) purecef.Task { return task }
168
169 var scheduled purecef.Task
170 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
171 require.Equal(t, purecef.ThreadIDTidUi, threadID)
172 require.NotNil(t, task)
173 if delayMs == 0 && scheduled == nil {
174 scheduled = task
175 }
176 return 1
177 }
178
179 host := &viewportSyncOrderHost{}
180 wv := &WebView{ctx: context.Background(), host: host}
181 wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
182
183 require.Empty(t, host.calls)
184 require.NotNil(t, scheduled)
185
186 scheduled.Execute()
187 require.Equal(t, []string{
188 "SetZoomLevel",
189 "NotifyScreenInfoChanged",
190 }, host.calls)
191}
192
193func TestSyncZoomForBackingScaleOnCEFUIThread_SkipsRedundantReapplyWhenBackingScaleUnchanged(t *testing.T) {
194 oldNewTask := cefNewTask
195 oldPostDelayedTask := cefPostDelayedTask
196 defer func() {
197 cefNewTask = oldNewTask
198 cefPostDelayedTask = oldPostDelayedTask
199 }()
200
201 cefNewTask = func(task purecef.Task) purecef.Task { return task }
202
203 var scheduled purecef.Task
204 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
205 if delayMs == 0 && scheduled == nil {
206 scheduled = task
207 }
208 return 1
209 }
210
211 host := &viewportSyncOrderHost{}
212 wv := &WebView{ctx: context.Background(), host: host}
213
214 wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
215 require.NotNil(t, scheduled)
216 scheduled.Execute()
217
218 host.calls = nil
219 scheduled = nil
220
221 wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
222 require.Nil(t, scheduled)
223 require.Empty(t, host.calls)
224}
225
226func TestNotifyViewportSyncOnCEFUIThread_SkipsStaleHost(t *testing.T) {
227 oldNewTask := cefNewTask
228 oldPostDelayedTask := cefPostDelayedTask
229 defer func() {
230 cefNewTask = oldNewTask
231 cefPostDelayedTask = oldPostDelayedTask
232 }()
233
234 cefNewTask = func(task purecef.Task) purecef.Task { return task }
235
236 var scheduled purecef.Task
237 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
238 scheduled = task
239 return 1
240 }
241
242 capturedHost := &viewportSyncOrderHost{}
243 currentHost := &viewportSyncOrderHost{}
244 wv := &WebView{ctx: context.Background(), host: currentHost}
245 wv.notifyViewportSyncOnCEFUIThread(capturedHost, true)
246
247 require.NotNil(t, scheduled)
248 scheduled.Execute()
249 require.Empty(t, capturedHost.calls)
250 require.Empty(t, currentHost.calls)
251}
252
253func TestNotifyViewportSyncOnCEFUIThread_SkipsDestroyedWebView(t *testing.T) {
254 oldNewTask := cefNewTask
255 oldPostDelayedTask := cefPostDelayedTask
256 defer func() {
257 cefNewTask = oldNewTask
258 cefPostDelayedTask = oldPostDelayedTask
259 }()
260
261 cefNewTask = func(task purecef.Task) purecef.Task { return task }
262
263 var scheduled purecef.Task
264 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
265 scheduled = task
266 return 1
267 }
268
269 host := &viewportSyncOrderHost{}
270 wv := &WebView{ctx: context.Background(), host: host}
271 wv.destroyed.Store(true)
272 wv.notifyViewportSyncOnCEFUIThread(host, true)
273
274 require.NotNil(t, scheduled)
275 scheduled.Execute()
276 require.Empty(t, host.calls)
277}
278
279func TestObservedViewportSizeReady(t *testing.T) {
280 tests := []struct {
281 name string
282 observedWidth int32
283 observedHeight int32
284 allocatedWidth int32
285 allocatedHeight int32
286 want bool
287 }{
288 {
289 name: "exact match",
290 observedWidth: 793,
291 observedHeight: 1753,
292 allocatedWidth: 793,
293 allocatedHeight: 1753,
294 want: true,
295 },
296 {
297 name: "one pixel drift is accepted",
298 observedWidth: 792,
299 observedHeight: 1754,
300 allocatedWidth: 793,
301 allocatedHeight: 1753,
302 want: true,
303 },
304 {
305 name: "fallback observed size is rejected",
306 observedWidth: 1,
307 observedHeight: 1,
308 allocatedWidth: 793,
309 allocatedHeight: 1753,
310 want: false,
311 },
312 {
313 name: "missing allocation is rejected",
314 observedWidth: 793,
315 observedHeight: 1753,
316 allocatedWidth: 0,
317 allocatedHeight: 1753,
318 want: false,
319 },
320 {
321 name: "large mismatch is rejected",
322 observedWidth: 1200,
323 observedHeight: 1400,
324 allocatedWidth: 1587,
325 allocatedHeight: 1679,
326 want: false,
327 },
328 }
329
330 for _, tt := range tests {
331 t.Run(tt.name, func(t *testing.T) {
332 require.Equal(t, tt.want, observedViewportSizeReady(tt.observedWidth, tt.observedHeight, tt.allocatedWidth, tt.allocatedHeight))
333 })
334 }
335}
336
337func TestPendingBrowserCreateObservedSize_RefreshesStaleFallbackSize(t *testing.T) {
338 bridge := &pendingBrowserCreateObservedSizeBridgeStub{
339 sizeWidth: 1,
340 sizeHeight: 1,
341 refreshWidth: 1055,
342 refreshHeight: 1719,
343 refreshProvided: true,
344 }
345
346 observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
347
348 require.True(t, ready)
349 require.Equal(t, int32(1055), observedWidth)
350 require.Equal(t, int32(1719), observedHeight)
351 require.Equal(t, 1, bridge.refreshCalls)
352}
353
354func TestPendingBrowserCreateObservedSize_SkipsRefreshWhenObservedSizeAlreadyMatches(t *testing.T) {
355 bridge := &pendingBrowserCreateObservedSizeBridgeStub{sizeWidth: 1055, sizeHeight: 1719}
356
357 observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
358
359 require.True(t, ready)
360 require.Equal(t, int32(1055), observedWidth)
361 require.Equal(t, int32(1719), observedHeight)
362 require.Equal(t, 0, bridge.refreshCalls)
363}
364
365func TestShouldDeferPendingBrowserCreateFromViewportSync_WaitsForNativePopupAttach(t *testing.T) {
366 wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
367
368 require.True(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
369
370 wv.nativePopupFallbackStarted = true
371 require.False(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
372}
373
374func TestPreparePendingBrowserCreateObservedSizeRetry_CoalescesScheduling(t *testing.T) {
375 oldScheduleAfter := cefScheduleAfter
376 defer func() { cefScheduleAfter = oldScheduleAfter }()
377
378 scheduleCalls := 0
379 cefScheduleAfter = func(delay time.Duration, fn func()) {
380 require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
381 require.NotNil(t, fn)
382 scheduleCalls++
383 }
384
385 wv := &WebView{pendingCreate: &pendingBrowserCreate{}}
386
387 attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
388 require.Equal(t, 1, attempt)
389 require.Equal(t, pendingBrowserCreateObservedSizeRetryScheduled, action)
390 require.Equal(t, 1, scheduleCalls)
391
392 attempt, action = wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
393 require.Equal(t, 1, attempt)
394 require.Equal(t, pendingBrowserCreateObservedSizeRetryAlreadyScheduled, action)
395 require.Equal(t, 1, scheduleCalls)
396}
397
398func TestPreparePendingBrowserCreateObservedSizeRetry_MaxRetriesProceedsWithoutScheduling(t *testing.T) {
399 oldScheduleAfter := cefScheduleAfter
400 defer func() { cefScheduleAfter = oldScheduleAfter }()
401
402 scheduleCalls := 0
403 cefScheduleAfter = func(delay time.Duration, fn func()) {
404 require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
405 require.NotNil(t, fn)
406 scheduleCalls++
407 }
408
409 wv := &WebView{pendingCreate: &pendingBrowserCreate{observedSizeRetries: pendingBrowserCreateObservedSizeMaxRetries}}
410
411 attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
412 require.Equal(t, pendingBrowserCreateObservedSizeMaxRetries+1, attempt)
413 require.Equal(t, pendingBrowserCreateObservedSizeRetryProceedWithoutDelay, action)
414 require.Equal(t, 0, scheduleCalls)
415 require.False(t, wv.pendingCreate.observedSizeRetryScheduled)
416}
417
418func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
419 oldNewTask := cefNewTask
420 oldPostDelayedTask := cefPostDelayedTask
421 defer func() {
422 cefNewTask = oldNewTask
423 cefPostDelayedTask = oldPostDelayedTask
424 }()
425
426 cefNewTask = func(task purecef.Task) purecef.Task { return task }
427
428 var scheduled []purecef.Task
429 var delays []int64
430 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
431 require.Equal(t, purecef.ThreadIDTidUi, threadID)
432 require.NotNil(t, task)
433 scheduled = append(scheduled, task)
434 delays = append(delays, delayMs)
435 return 1
436 }
437
438 host := &viewportSyncOrderHost{}
439 wv := &WebView{ctx: context.Background(), host: host}
440
441 wv.scheduleResizeRepaintPulse(context.Background(), "first")
442 wv.scheduleResizeRepaintPulse(context.Background(), "second")
443
444 require.Equal(t, []int64{16, 48, 16, 48}, delays)
445 require.Len(t, scheduled, 4)
446
447 scheduled[0].Execute()
448 scheduled[1].Execute()
449 require.Empty(t, host.calls)
450
451 scheduled[2].Execute()
452 scheduled[3].Execute()
453 require.Equal(t, []string{
454 "NotifyScreenInfoChanged",
455 "WasResized",
456 "Invalidate",
457 "NotifyScreenInfoChanged",
458 "WasResized",
459 "Invalidate",
460 }, host.calls)
461}
462
463func TestScheduleResizeRepaintPulse_SkipsStaleHost(t *testing.T) {
464 oldNewTask := cefNewTask
465 oldPostDelayedTask := cefPostDelayedTask
466 defer func() {
467 cefNewTask = oldNewTask
468 cefPostDelayedTask = oldPostDelayedTask
469 }()
470
471 cefNewTask = func(task purecef.Task) purecef.Task { return task }
472
473 var scheduled []purecef.Task
474 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
475 require.Equal(t, purecef.ThreadIDTidUi, threadID)
476 require.NotNil(t, task)
477 scheduled = append(scheduled, task)
478 return 1
479 }
480
481 capturedHost := &viewportSyncOrderHost{}
482 currentHost := &viewportSyncOrderHost{}
483 wv := &WebView{ctx: context.Background(), host: capturedHost}
484
485 wv.scheduleResizeRepaintPulse(context.Background(), "stale-host")
486 wv.host = currentHost
487
488 require.Len(t, scheduled, 2)
489 scheduled[0].Execute()
490 scheduled[1].Execute()
491
492 require.Empty(t, capturedHost.calls)
493 require.Empty(t, currentHost.calls)
494}
495
496func TestScheduleResizeRepaintPulse_SkipsWhenHostNilAtScheduleTime(t *testing.T) {
497 oldNewTask := cefNewTask
498 oldPostDelayedTask := cefPostDelayedTask
499 defer func() {
500 cefNewTask = oldNewTask
501 cefPostDelayedTask = oldPostDelayedTask
502 }()
503
504 cefNewTask = func(task purecef.Task) purecef.Task { return task }
505
506 var scheduled []purecef.Task
507 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
508 scheduled = append(scheduled, task)
509 return 1
510 }
511
512 wv := &WebView{ctx: context.Background()}
513
514 wv.scheduleResizeRepaintPulse(context.Background(), "nil-host")
515
516 require.Empty(t, scheduled)
517}
518
519func TestScheduleResizeRepaintPulse_SkipsWhenHostClearedBeforeExecution(t *testing.T) {
520 oldNewTask := cefNewTask
521 oldPostDelayedTask := cefPostDelayedTask
522 defer func() {
523 cefNewTask = oldNewTask
524 cefPostDelayedTask = oldPostDelayedTask
525 }()
526
527 cefNewTask = func(task purecef.Task) purecef.Task { return task }
528
529 var scheduled []purecef.Task
530 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
531 require.Equal(t, purecef.ThreadIDTidUi, threadID)
532 require.NotNil(t, task)
533 scheduled = append(scheduled, task)
534 return 1
535 }
536
537 host := &viewportSyncOrderHost{}
538 wv := &WebView{ctx: context.Background(), host: host}
539
540 wv.scheduleResizeRepaintPulse(context.Background(), "nil-host-before-execute")
541 wv.host = nil
542
543 require.Len(t, scheduled, 2)
544 scheduled[0].Execute()
545 scheduled[1].Execute()
546
547 require.Empty(t, host.calls)
548}