fix(cef): use released size observer
- commit
- e5061b62111cd696977df15faccb2075a2cb5ea7
- author
- brice · · 2026-05-24 13:18 UTC
- parent
- c48b63f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +279 -34
M go.mod +1 -1
| @@ -7,7 +7,7 @@ require ( | |||
| 7 | 7 | github.com/andybalholm/brotli v1.2.1 | |
| 8 | 8 | github.com/bnema/purego v0.11.0-bnema.2 | |
| 9 | 9 | github.com/bnema/purego-cef v0.13.0 | |
| 10 | - | github.com/bnema/purego-cef2gtk v0.5.2 | |
| 10 | + | github.com/bnema/purego-cef2gtk v0.5.3 | |
| 11 | 11 | github.com/bnema/purego-pipewire v0.1.3 | |
| 12 | 12 | github.com/bnema/purego-sqlite v0.1.2 | |
| 13 | 13 | github.com/bnema/puregotk v0.5.2 | |
| @@ -7,7 +7,7 @@ require ( | |||||
| 7 | github.com/andybalholm/brotli v1.2.1 | 7 | github.com/andybalholm/brotli v1.2.1 | ||
| 8 | github.com/bnema/purego v0.11.0-bnema.2 | 8 | github.com/bnema/purego v0.11.0-bnema.2 | ||
| 9 | github.com/bnema/purego-cef v0.13.0 | 9 | github.com/bnema/purego-cef v0.13.0 | ||
| 10 | - | github.com/bnema/purego-cef2gtk v0.5.2 | 10 | + | github.com/bnema/purego-cef2gtk v0.5.3 |
| 11 | github.com/bnema/purego-pipewire v0.1.3 | 11 | github.com/bnema/purego-pipewire v0.1.3 | ||
| 12 | github.com/bnema/purego-sqlite v0.1.2 | 12 | github.com/bnema/purego-sqlite v0.1.2 | ||
| 13 | github.com/bnema/puregotk v0.5.2 | 13 | github.com/bnema/puregotk v0.5.2 | ||
M go.sum +2 -2 generated file: collapsed
M internal/infrastructure/cef/factory.go +98 -31
| @@ -177,18 +177,7 @@ func (f *WebViewFactory) configureInitialBrowserCreation( | |||
| 177 | 177 | // the one-shot initial resize path; once that path has been consumed, | |
| 178 | 178 | // later size events must go through normal viewport sync even if popup | |
| 179 | 179 | // attachment keeps pendingCreate alive a little longer. | |
| 180 | - | if wv.shouldStartBrowserCreateFromSizeObserver() { | |
| 181 | - | if err := wv.viewBridge.PrepareOnGTKThread(); err != nil { | |
| 182 | - | logging.FromContext(ctx).Warn(). | |
| 183 | - | Err(err). | |
| 184 | - | Uint64("webview_id", uint64(wv.id)). | |
| 185 | - | Int32("resize_width", w). | |
| 186 | - | Int32("resize_height", h). | |
| 187 | - | Msg("cef: cef2gtk view not ready after resize, will retry") | |
| 188 | - | return | |
| 189 | - | } | |
| 190 | - | wv.markInitialBrowserCreateResizeHandled() | |
| 191 | - | onFirstResize(w, h) | |
| 180 | + | if f.handleInitialBrowserCreateSizeObserver(ctx, wv, onFirstResize, wv.viewBridge.PrepareOnGTKThread, w, h) { | |
| 192 | 181 | return | |
| 193 | 182 | } | |
| 194 | 183 | ||
| @@ -203,6 +192,49 @@ func (f *WebViewFactory) configureInitialBrowserCreation( | |||
| 203 | 192 | }) | |
| 204 | 193 | } | |
| 205 | 194 | ||
| 195 | + | func (f *WebViewFactory) handleInitialBrowserCreateSizeObserver( | |
| 196 | + | ctx context.Context, | |
| 197 | + | wv *WebView, | |
| 198 | + | onFirstResize func(w, h int32), | |
| 199 | + | prepareOnGTK func() error, | |
| 200 | + | w, h int32, | |
| 201 | + | ) bool { | |
| 202 | + | if wv == nil || !wv.shouldStartBrowserCreateFromSizeObserver() { | |
| 203 | + | return false | |
| 204 | + | } | |
| 205 | + | if !initialBrowserCreateSizeReadyFromObserver(w, h) { | |
| 206 | + | if wv.awaitsNativePopupAttachment() && onFirstResize != nil { | |
| 207 | + | onFirstResize(w, h) | |
| 208 | + | } | |
| 209 | + | logging.FromContext(ctx).Debug(). | |
| 210 | + | Uint64("webview_id", uint64(wv.id)). | |
| 211 | + | Int32("resize_width", w). | |
| 212 | + | Int32("resize_height", h). | |
| 213 | + | Msg("cef: ignoring bootstrap size observer event before real view size is ready") | |
| 214 | + | return true | |
| 215 | + | } | |
| 216 | + | if prepareOnGTK != nil { | |
| 217 | + | if err := prepareOnGTK(); err != nil { | |
| 218 | + | logging.FromContext(ctx).Warn(). | |
| 219 | + | Err(err). | |
| 220 | + | Uint64("webview_id", uint64(wv.id)). | |
| 221 | + | Int32("resize_width", w). | |
| 222 | + | Int32("resize_height", h). | |
| 223 | + | Msg("cef: cef2gtk view not ready after resize, will retry") | |
| 224 | + | return true | |
| 225 | + | } | |
| 226 | + | } | |
| 227 | + | wv.markInitialBrowserCreateResizeHandled() | |
| 228 | + | if onFirstResize != nil { | |
| 229 | + | onFirstResize(w, h) | |
| 230 | + | } | |
| 231 | + | return true | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | func initialBrowserCreateSizeReadyFromObserver(width, height int32) bool { | |
| 235 | + | return observedViewportSizeReady(width, height, width, height) | |
| 236 | + | } | |
| 237 | + | ||
| 206 | 238 | func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebView, w, h int32) { | |
| 207 | 239 | log := logging.FromContext(ctx) | |
| 208 | 240 | pc := wv.takePendingCreate() | |
| @@ -329,26 +361,61 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie | |||
| 329 | 361 | } | |
| 330 | 362 | ||
| 331 | 363 | f.configureInitialBrowserCreation(ctx, popupWV, popupWV.client, &windowInfo, &settings, func(w, h int32) { | |
| 332 | - | log := logging.FromContext(ctx) | |
| 333 | - | if popupWV.awaitsNativePopupAttachment() { | |
| 334 | - | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, awaiting native popup attach") | |
| 335 | - | popupWV.scheduleNativePopupFallback(nativePopupAttachFallbackDelay, func() { | |
| 336 | - | if !popupWV.startNativePopupFallback() { | |
| 337 | - | return | |
| 338 | - | } | |
| 339 | - | logging.FromContext(ctx).Warn(). | |
| 340 | - | Uint64("webview_id", uint64(popupWV.id)). | |
| 341 | - | Msg("cef: native popup attach timed out, creating popup browser directly") | |
| 342 | - | f.postPendingBrowserCreate(ctx, popupWV, w, h) | |
| 343 | - | }) | |
| 344 | - | return | |
| 345 | - | } | |
| 364 | + | f.handlePopupShellInitialResize(ctx, popupWV, func(w, h int32) { | |
| 365 | + | f.postPendingBrowserCreate(ctx, popupWV, w, h) | |
| 366 | + | }, w, h) | |
| 367 | + | }) | |
| 368 | + | return popupWV, nil | |
| 369 | + | } | |
| 346 | 370 | ||
| 347 | - | if !popupWV.preparePopupShellDirectBrowserCreation() { | |
| 348 | - | return | |
| 371 | + | func (f *WebViewFactory) handlePopupShellInitialResize( | |
| 372 | + | ctx context.Context, | |
| 373 | + | popupWV *WebView, | |
| 374 | + | postPendingCreate func(w, h int32), | |
| 375 | + | w, h int32, | |
| 376 | + | ) { | |
| 377 | + | if popupWV == nil { | |
| 378 | + | return | |
| 379 | + | } | |
| 380 | + | log := logging.FromContext(ctx) | |
| 381 | + | if popupWV.awaitsNativePopupAttachment() { | |
| 382 | + | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell initial size observed, awaiting native popup attach") | |
| 383 | + | f.schedulePopupShellNativeFallback(ctx, popupWV) | |
| 384 | + | return | |
| 385 | + | } | |
| 386 | + | if popupWV.awaitsBrowserCreateFromNativePopupFallback() { | |
| 387 | + | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell size ready after fallback, creating browser directly") | |
| 388 | + | if postPendingCreate != nil { | |
| 389 | + | postPendingCreate(w, h) | |
| 349 | 390 | } | |
| 350 | - | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, creating browser directly") | |
| 351 | - | f.postPendingBrowserCreate(ctx, popupWV, w, h) | |
| 391 | + | return | |
| 392 | + | } | |
| 393 | + | if !popupWV.preparePopupShellDirectBrowserCreation() { | |
| 394 | + | return | |
| 395 | + | } | |
| 396 | + | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, creating browser directly") | |
| 397 | + | if postPendingCreate != nil { | |
| 398 | + | postPendingCreate(w, h) | |
| 399 | + | } | |
| 400 | + | } | |
| 401 | + | ||
| 402 | + | func (f *WebViewFactory) schedulePopupShellNativeFallback(ctx context.Context, popupWV *WebView) { | |
| 403 | + | if popupWV == nil { | |
| 404 | + | return | |
| 405 | + | } | |
| 406 | + | popupWV.scheduleNativePopupFallback(nativePopupAttachFallbackDelay, func() { | |
| 407 | + | popupWV.runOnGTK(func() { | |
| 408 | + | if !popupWV.startNativePopupFallback() { | |
| 409 | + | return | |
| 410 | + | } | |
| 411 | + | logging.FromContext(ctx).Warn(). | |
| 412 | + | Uint64("webview_id", uint64(popupWV.id)). | |
| 413 | + | Msg("cef: native popup attach timed out, starting direct popup fallback") | |
| 414 | + | synced := popupWV.syncViewportNowOnGTK(ctx, "native-popup-fallback-timeout") | |
| 415 | + | logging.FromContext(ctx).Debug(). | |
| 416 | + | Uint64("webview_id", uint64(popupWV.id)). | |
| 417 | + | Bool("browser_ready", synced). | |
| 418 | + | Msg("cef: popup fallback viewport sync requested") | |
| 419 | + | }) | |
| 352 | 420 | }) | |
| 353 | - | return popupWV, nil | |
| 354 | 421 | } | |
| @@ -177,18 +177,7 @@ func (f *WebViewFactory) configureInitialBrowserCreation( | |||||
| 177 | // the one-shot initial resize path; once that path has been consumed, | 177 | // the one-shot initial resize path; once that path has been consumed, | ||
| 178 | // later size events must go through normal viewport sync even if popup | 178 | // later size events must go through normal viewport sync even if popup | ||
| 179 | // attachment keeps pendingCreate alive a little longer. | 179 | // attachment keeps pendingCreate alive a little longer. | ||
| 180 | - | if wv.shouldStartBrowserCreateFromSizeObserver() { | 180 | + | if f.handleInitialBrowserCreateSizeObserver(ctx, wv, onFirstResize, wv.viewBridge.PrepareOnGTKThread, w, h) { |
| 181 | - | if err := wv.viewBridge.PrepareOnGTKThread(); err != nil { | |||
| 182 | - | logging.FromContext(ctx).Warn(). | |||
| 183 | - | Err(err). | |||
| 184 | - | Uint64("webview_id", uint64(wv.id)). | |||
| 185 | - | Int32("resize_width", w). | |||
| 186 | - | Int32("resize_height", h). | |||
| 187 | - | Msg("cef: cef2gtk view not ready after resize, will retry") | |||
| 188 | - | return | |||
| 189 | - | } | |||
| 190 | - | wv.markInitialBrowserCreateResizeHandled() | |||
| 191 | - | onFirstResize(w, h) | |||
| 192 | return | 181 | return | ||
| 193 | } | 182 | } | ||
| 194 | 183 | ||||
| @@ -203,6 +192,49 @@ func (f *WebViewFactory) configureInitialBrowserCreation( | |||||
| 203 | }) | 192 | }) | ||
| 204 | } | 193 | } | ||
| 205 | 194 | ||||
| 195 | + | func (f *WebViewFactory) handleInitialBrowserCreateSizeObserver( | |||
| 196 | + | ctx context.Context, | |||
| 197 | + | wv *WebView, | |||
| 198 | + | onFirstResize func(w, h int32), | |||
| 199 | + | prepareOnGTK func() error, | |||
| 200 | + | w, h int32, | |||
| 201 | + | ) bool { | |||
| 202 | + | if wv == nil || !wv.shouldStartBrowserCreateFromSizeObserver() { | |||
| 203 | + | return false | |||
| 204 | + | } | |||
| 205 | + | if !initialBrowserCreateSizeReadyFromObserver(w, h) { | |||
| 206 | + | if wv.awaitsNativePopupAttachment() && onFirstResize != nil { | |||
| 207 | + | onFirstResize(w, h) | |||
| 208 | + | } | |||
| 209 | + | logging.FromContext(ctx).Debug(). | |||
| 210 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 211 | + | Int32("resize_width", w). | |||
| 212 | + | Int32("resize_height", h). | |||
| 213 | + | Msg("cef: ignoring bootstrap size observer event before real view size is ready") | |||
| 214 | + | return true | |||
| 215 | + | } | |||
| 216 | + | if prepareOnGTK != nil { | |||
| 217 | + | if err := prepareOnGTK(); err != nil { | |||
| 218 | + | logging.FromContext(ctx).Warn(). | |||
| 219 | + | Err(err). | |||
| 220 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 221 | + | Int32("resize_width", w). | |||
| 222 | + | Int32("resize_height", h). | |||
| 223 | + | Msg("cef: cef2gtk view not ready after resize, will retry") | |||
| 224 | + | return true | |||
| 225 | + | } | |||
| 226 | + | } | |||
| 227 | + | wv.markInitialBrowserCreateResizeHandled() | |||
| 228 | + | if onFirstResize != nil { | |||
| 229 | + | onFirstResize(w, h) | |||
| 230 | + | } | |||
| 231 | + | return true | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | func initialBrowserCreateSizeReadyFromObserver(width, height int32) bool { | |||
| 235 | + | return observedViewportSizeReady(width, height, width, height) | |||
| 236 | + | } | |||
| 237 | + | ||||
| 206 | func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebView, w, h int32) { | 238 | func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebView, w, h int32) { | ||
| 207 | log := logging.FromContext(ctx) | 239 | log := logging.FromContext(ctx) | ||
| 208 | pc := wv.takePendingCreate() | 240 | pc := wv.takePendingCreate() | ||
| @@ -329,26 +361,61 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie | |||||
| 329 | } | 361 | } | ||
| 330 | 362 | ||||
| 331 | f.configureInitialBrowserCreation(ctx, popupWV, popupWV.client, &windowInfo, &settings, func(w, h int32) { | 363 | f.configureInitialBrowserCreation(ctx, popupWV, popupWV.client, &windowInfo, &settings, func(w, h int32) { | ||
| 332 | - | log := logging.FromContext(ctx) | 364 | + | f.handlePopupShellInitialResize(ctx, popupWV, func(w, h int32) { |
| 333 | - | if popupWV.awaitsNativePopupAttachment() { | 365 | + | f.postPendingBrowserCreate(ctx, popupWV, w, h) |
| 334 | - | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, awaiting native popup attach") | 366 | + | }, w, h) |
| 335 | - | popupWV.scheduleNativePopupFallback(nativePopupAttachFallbackDelay, func() { | 367 | + | }) |
| 336 | - | if !popupWV.startNativePopupFallback() { | 368 | + | return popupWV, nil |
| 337 | - | return | 369 | + | } |
| 338 | - | } | |||
| 339 | - | logging.FromContext(ctx).Warn(). | |||
| 340 | - | Uint64("webview_id", uint64(popupWV.id)). | |||
| 341 | - | Msg("cef: native popup attach timed out, creating popup browser directly") | |||
| 342 | - | f.postPendingBrowserCreate(ctx, popupWV, w, h) | |||
| 343 | - | }) | |||
| 344 | - | return | |||
| 345 | - | } | |||
| 346 | 370 | ||||
| 347 | - | if !popupWV.preparePopupShellDirectBrowserCreation() { | 371 | + | func (f *WebViewFactory) handlePopupShellInitialResize( |
| 348 | - | return | 372 | + | ctx context.Context, |
| 373 | + | popupWV *WebView, | |||
| 374 | + | postPendingCreate func(w, h int32), | |||
| 375 | + | w, h int32, | |||
| 376 | + | ) { | |||
| 377 | + | if popupWV == nil { | |||
| 378 | + | return | |||
| 379 | + | } | |||
| 380 | + | log := logging.FromContext(ctx) | |||
| 381 | + | if popupWV.awaitsNativePopupAttachment() { | |||
| 382 | + | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell initial size observed, awaiting native popup attach") | |||
| 383 | + | f.schedulePopupShellNativeFallback(ctx, popupWV) | |||
| 384 | + | return | |||
| 385 | + | } | |||
| 386 | + | if popupWV.awaitsBrowserCreateFromNativePopupFallback() { | |||
| 387 | + | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell size ready after fallback, creating browser directly") | |||
| 388 | + | if postPendingCreate != nil { | |||
| 389 | + | postPendingCreate(w, h) | |||
| 349 | } | 390 | } | ||
| 350 | - | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, creating browser directly") | 391 | + | return |
| 351 | - | f.postPendingBrowserCreate(ctx, popupWV, w, h) | 392 | + | } |
| 393 | + | if !popupWV.preparePopupShellDirectBrowserCreation() { | |||
| 394 | + | return | |||
| 395 | + | } | |||
| 396 | + | log.Debug().Int32("w", w).Int32("h", h).Msg("cef: popup shell first resize, creating browser directly") | |||
| 397 | + | if postPendingCreate != nil { | |||
| 398 | + | postPendingCreate(w, h) | |||
| 399 | + | } | |||
| 400 | + | } | |||
| 401 | + | ||||
| 402 | + | func (f *WebViewFactory) schedulePopupShellNativeFallback(ctx context.Context, popupWV *WebView) { | |||
| 403 | + | if popupWV == nil { | |||
| 404 | + | return | |||
| 405 | + | } | |||
| 406 | + | popupWV.scheduleNativePopupFallback(nativePopupAttachFallbackDelay, func() { | |||
| 407 | + | popupWV.runOnGTK(func() { | |||
| 408 | + | if !popupWV.startNativePopupFallback() { | |||
| 409 | + | return | |||
| 410 | + | } | |||
| 411 | + | logging.FromContext(ctx).Warn(). | |||
| 412 | + | Uint64("webview_id", uint64(popupWV.id)). | |||
| 413 | + | Msg("cef: native popup attach timed out, starting direct popup fallback") | |||
| 414 | + | synced := popupWV.syncViewportNowOnGTK(ctx, "native-popup-fallback-timeout") | |||
| 415 | + | logging.FromContext(ctx).Debug(). | |||
| 416 | + | Uint64("webview_id", uint64(popupWV.id)). | |||
| 417 | + | Bool("browser_ready", synced). | |||
| 418 | + | Msg("cef: popup fallback viewport sync requested") | |||
| 419 | + | }) | |||
| 352 | }) | 420 | }) | ||
| 353 | - | return popupWV, nil | |||
| 354 | } | 421 | } | ||
M internal/infrastructure/cef/factory_resize_test.go +169 -0
| @@ -55,6 +55,175 @@ func TestShouldStartBrowserCreateFromSizeObserver_FalseAfterPendingCreateConsume | |||
| 55 | 55 | require.False(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | + | func TestInitialBrowserCreateSizeReadyFromObserver(t *testing.T) { | |
| 59 | + | tests := []struct { | |
| 60 | + | name string | |
| 61 | + | width int32 | |
| 62 | + | height int32 | |
| 63 | + | want bool | |
| 64 | + | }{ | |
| 65 | + | {name: "rejects bootstrap one by one", width: 1, height: 1, want: false}, | |
| 66 | + | {name: "rejects zero width", width: 0, height: 480, want: false}, | |
| 67 | + | {name: "rejects one pixel height", width: 640, height: 1, want: false}, | |
| 68 | + | {name: "accepts real positive size", width: 640, height: 480, want: true}, | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | for _, tt := range tests { | |
| 72 | + | t.Run(tt.name, func(t *testing.T) { | |
| 73 | + | require.Equal(t, tt.want, initialBrowserCreateSizeReadyFromObserver(tt.width, tt.height)) | |
| 74 | + | }) | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func TestHandleInitialBrowserCreateSizeObserver_IgnoresBootstrapForNormalWebView(t *testing.T) { | |
| 79 | + | factory := &WebViewFactory{} | |
| 80 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}} | |
| 81 | + | prepared := false | |
| 82 | + | resized := false | |
| 83 | + | ||
| 84 | + | handled := factory.handleInitialBrowserCreateSizeObserver( | |
| 85 | + | context.Background(), | |
| 86 | + | wv, | |
| 87 | + | func(int32, int32) { resized = true }, | |
| 88 | + | func() error { | |
| 89 | + | prepared = true | |
| 90 | + | return nil | |
| 91 | + | }, | |
| 92 | + | 1, | |
| 93 | + | 1, | |
| 94 | + | ) | |
| 95 | + | ||
| 96 | + | require.True(t, handled) | |
| 97 | + | require.False(t, prepared) | |
| 98 | + | require.False(t, resized) | |
| 99 | + | require.True(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func TestHandleInitialBrowserCreateSizeObserver_BootstrapPopupArmsFallbackWithoutPreparing(t *testing.T) { | |
| 103 | + | factory := &WebViewFactory{} | |
| 104 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true} | |
| 105 | + | prepared := false | |
| 106 | + | var scheduled func() | |
| 107 | + | wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer { | |
| 108 | + | scheduled = fn | |
| 109 | + | return stubStoppableTimer{} | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | handled := factory.handleInitialBrowserCreateSizeObserver( | |
| 113 | + | context.Background(), | |
| 114 | + | wv, | |
| 115 | + | func(w, h int32) { | |
| 116 | + | factory.handlePopupShellInitialResize(context.Background(), wv, nil, w, h) | |
| 117 | + | }, | |
| 118 | + | func() error { | |
| 119 | + | prepared = true | |
| 120 | + | return nil | |
| 121 | + | }, | |
| 122 | + | 1, | |
| 123 | + | 1, | |
| 124 | + | ) | |
| 125 | + | ||
| 126 | + | require.True(t, handled) | |
| 127 | + | require.False(t, prepared) | |
| 128 | + | require.NotNil(t, scheduled) | |
| 129 | + | require.True(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func TestHandleInitialBrowserCreateSizeObserver_ReadySizePreparesAndConsumesInitialResize(t *testing.T) { | |
| 133 | + | factory := &WebViewFactory{} | |
| 134 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}} | |
| 135 | + | prepared := 0 | |
| 136 | + | resizes := 0 | |
| 137 | + | ||
| 138 | + | handled := factory.handleInitialBrowserCreateSizeObserver( | |
| 139 | + | context.Background(), | |
| 140 | + | wv, | |
| 141 | + | func(int32, int32) { resizes++ }, | |
| 142 | + | func() error { | |
| 143 | + | prepared++ | |
| 144 | + | return nil | |
| 145 | + | }, | |
| 146 | + | 640, | |
| 147 | + | 480, | |
| 148 | + | ) | |
| 149 | + | ||
| 150 | + | require.True(t, handled) | |
| 151 | + | require.Equal(t, 1, prepared) | |
| 152 | + | require.Equal(t, 1, resizes) | |
| 153 | + | require.False(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func TestHandleInitialBrowserCreateSizeObserver_ReadySizeAfterPopupFallbackPostsCreate(t *testing.T) { | |
| 157 | + | factory := &WebViewFactory{} | |
| 158 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true} | |
| 159 | + | prepared := 0 | |
| 160 | + | posted := 0 | |
| 161 | + | var scheduled func() | |
| 162 | + | wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer { | |
| 163 | + | scheduled = fn | |
| 164 | + | return stubStoppableTimer{} | |
| 165 | + | } | |
| 166 | + | onFirstResize := func(w, h int32) { | |
| 167 | + | factory.handlePopupShellInitialResize(context.Background(), wv, func(int32, int32) { | |
| 168 | + | posted++ | |
| 169 | + | }, w, h) | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | handled := factory.handleInitialBrowserCreateSizeObserver( | |
| 173 | + | context.Background(), | |
| 174 | + | wv, | |
| 175 | + | onFirstResize, | |
| 176 | + | func() error { | |
| 177 | + | prepared++ | |
| 178 | + | return nil | |
| 179 | + | }, | |
| 180 | + | 0, | |
| 181 | + | 480, | |
| 182 | + | ) | |
| 183 | + | require.True(t, handled) | |
| 184 | + | require.Equal(t, 0, prepared) | |
| 185 | + | require.Equal(t, 0, posted) | |
| 186 | + | require.NotNil(t, scheduled) | |
| 187 | + | require.True(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |
| 188 | + | ||
| 189 | + | scheduled() | |
| 190 | + | require.True(t, wv.awaitsBrowserCreateFromNativePopupFallback()) | |
| 191 | + | ||
| 192 | + | handled = factory.handleInitialBrowserCreateSizeObserver( | |
| 193 | + | context.Background(), | |
| 194 | + | wv, | |
| 195 | + | onFirstResize, | |
| 196 | + | func() error { | |
| 197 | + | prepared++ | |
| 198 | + | return nil | |
| 199 | + | }, | |
| 200 | + | 640, | |
| 201 | + | 480, | |
| 202 | + | ) | |
| 203 | + | require.True(t, handled) | |
| 204 | + | require.Equal(t, 1, prepared) | |
| 205 | + | require.Equal(t, 1, posted) | |
| 206 | + | require.False(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func TestSchedulePopupShellNativeFallback_StartsFallbackOnTimeout(t *testing.T) { | |
| 210 | + | factory := &WebViewFactory{} | |
| 211 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true} | |
| 212 | + | var scheduled func() | |
| 213 | + | wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer { | |
| 214 | + | scheduled = fn | |
| 215 | + | return stubStoppableTimer{} | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | factory.schedulePopupShellNativeFallback(context.Background(), wv) | |
| 219 | + | require.NotNil(t, scheduled) | |
| 220 | + | ||
| 221 | + | scheduled() | |
| 222 | + | ||
| 223 | + | require.True(t, wv.nativePopupFallbackStarted) | |
| 224 | + | require.False(t, wv.nativePopupCandidate) | |
| 225 | + | } | |
| 226 | + | ||
| 58 | 227 | func TestPostPendingBrowserCreate_RetriesWhenPostingTaskFails(t *testing.T) { | |
| 59 | 228 | oldNewTask := cefNewTask | |
| 60 | 229 | oldPostTask := cefPostTask | |
| @@ -55,6 +55,175 @@ func TestShouldStartBrowserCreateFromSizeObserver_FalseAfterPendingCreateConsume | |||||
| 55 | require.False(t, wv.shouldStartBrowserCreateFromSizeObserver()) | 55 | require.False(t, wv.shouldStartBrowserCreateFromSizeObserver()) | ||
| 56 | } | 56 | } | ||
| 57 | 57 | ||||
| 58 | + | func TestInitialBrowserCreateSizeReadyFromObserver(t *testing.T) { | |||
| 59 | + | tests := []struct { | |||
| 60 | + | name string | |||
| 61 | + | width int32 | |||
| 62 | + | height int32 | |||
| 63 | + | want bool | |||
| 64 | + | }{ | |||
| 65 | + | {name: "rejects bootstrap one by one", width: 1, height: 1, want: false}, | |||
| 66 | + | {name: "rejects zero width", width: 0, height: 480, want: false}, | |||
| 67 | + | {name: "rejects one pixel height", width: 640, height: 1, want: false}, | |||
| 68 | + | {name: "accepts real positive size", width: 640, height: 480, want: true}, | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | for _, tt := range tests { | |||
| 72 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 73 | + | require.Equal(t, tt.want, initialBrowserCreateSizeReadyFromObserver(tt.width, tt.height)) | |||
| 74 | + | }) | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func TestHandleInitialBrowserCreateSizeObserver_IgnoresBootstrapForNormalWebView(t *testing.T) { | |||
| 79 | + | factory := &WebViewFactory{} | |||
| 80 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}} | |||
| 81 | + | prepared := false | |||
| 82 | + | resized := false | |||
| 83 | + | ||||
| 84 | + | handled := factory.handleInitialBrowserCreateSizeObserver( | |||
| 85 | + | context.Background(), | |||
| 86 | + | wv, | |||
| 87 | + | func(int32, int32) { resized = true }, | |||
| 88 | + | func() error { | |||
| 89 | + | prepared = true | |||
| 90 | + | return nil | |||
| 91 | + | }, | |||
| 92 | + | 1, | |||
| 93 | + | 1, | |||
| 94 | + | ) | |||
| 95 | + | ||||
| 96 | + | require.True(t, handled) | |||
| 97 | + | require.False(t, prepared) | |||
| 98 | + | require.False(t, resized) | |||
| 99 | + | require.True(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func TestHandleInitialBrowserCreateSizeObserver_BootstrapPopupArmsFallbackWithoutPreparing(t *testing.T) { | |||
| 103 | + | factory := &WebViewFactory{} | |||
| 104 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true} | |||
| 105 | + | prepared := false | |||
| 106 | + | var scheduled func() | |||
| 107 | + | wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer { | |||
| 108 | + | scheduled = fn | |||
| 109 | + | return stubStoppableTimer{} | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | handled := factory.handleInitialBrowserCreateSizeObserver( | |||
| 113 | + | context.Background(), | |||
| 114 | + | wv, | |||
| 115 | + | func(w, h int32) { | |||
| 116 | + | factory.handlePopupShellInitialResize(context.Background(), wv, nil, w, h) | |||
| 117 | + | }, | |||
| 118 | + | func() error { | |||
| 119 | + | prepared = true | |||
| 120 | + | return nil | |||
| 121 | + | }, | |||
| 122 | + | 1, | |||
| 123 | + | 1, | |||
| 124 | + | ) | |||
| 125 | + | ||||
| 126 | + | require.True(t, handled) | |||
| 127 | + | require.False(t, prepared) | |||
| 128 | + | require.NotNil(t, scheduled) | |||
| 129 | + | require.True(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | func TestHandleInitialBrowserCreateSizeObserver_ReadySizePreparesAndConsumesInitialResize(t *testing.T) { | |||
| 133 | + | factory := &WebViewFactory{} | |||
| 134 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}} | |||
| 135 | + | prepared := 0 | |||
| 136 | + | resizes := 0 | |||
| 137 | + | ||||
| 138 | + | handled := factory.handleInitialBrowserCreateSizeObserver( | |||
| 139 | + | context.Background(), | |||
| 140 | + | wv, | |||
| 141 | + | func(int32, int32) { resizes++ }, | |||
| 142 | + | func() error { | |||
| 143 | + | prepared++ | |||
| 144 | + | return nil | |||
| 145 | + | }, | |||
| 146 | + | 640, | |||
| 147 | + | 480, | |||
| 148 | + | ) | |||
| 149 | + | ||||
| 150 | + | require.True(t, handled) | |||
| 151 | + | require.Equal(t, 1, prepared) | |||
| 152 | + | require.Equal(t, 1, resizes) | |||
| 153 | + | require.False(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func TestHandleInitialBrowserCreateSizeObserver_ReadySizeAfterPopupFallbackPostsCreate(t *testing.T) { | |||
| 157 | + | factory := &WebViewFactory{} | |||
| 158 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true} | |||
| 159 | + | prepared := 0 | |||
| 160 | + | posted := 0 | |||
| 161 | + | var scheduled func() | |||
| 162 | + | wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer { | |||
| 163 | + | scheduled = fn | |||
| 164 | + | return stubStoppableTimer{} | |||
| 165 | + | } | |||
| 166 | + | onFirstResize := func(w, h int32) { | |||
| 167 | + | factory.handlePopupShellInitialResize(context.Background(), wv, func(int32, int32) { | |||
| 168 | + | posted++ | |||
| 169 | + | }, w, h) | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | handled := factory.handleInitialBrowserCreateSizeObserver( | |||
| 173 | + | context.Background(), | |||
| 174 | + | wv, | |||
| 175 | + | onFirstResize, | |||
| 176 | + | func() error { | |||
| 177 | + | prepared++ | |||
| 178 | + | return nil | |||
| 179 | + | }, | |||
| 180 | + | 0, | |||
| 181 | + | 480, | |||
| 182 | + | ) | |||
| 183 | + | require.True(t, handled) | |||
| 184 | + | require.Equal(t, 0, prepared) | |||
| 185 | + | require.Equal(t, 0, posted) | |||
| 186 | + | require.NotNil(t, scheduled) | |||
| 187 | + | require.True(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |||
| 188 | + | ||||
| 189 | + | scheduled() | |||
| 190 | + | require.True(t, wv.awaitsBrowserCreateFromNativePopupFallback()) | |||
| 191 | + | ||||
| 192 | + | handled = factory.handleInitialBrowserCreateSizeObserver( | |||
| 193 | + | context.Background(), | |||
| 194 | + | wv, | |||
| 195 | + | onFirstResize, | |||
| 196 | + | func() error { | |||
| 197 | + | prepared++ | |||
| 198 | + | return nil | |||
| 199 | + | }, | |||
| 200 | + | 640, | |||
| 201 | + | 480, | |||
| 202 | + | ) | |||
| 203 | + | require.True(t, handled) | |||
| 204 | + | require.Equal(t, 1, prepared) | |||
| 205 | + | require.Equal(t, 1, posted) | |||
| 206 | + | require.False(t, wv.shouldStartBrowserCreateFromSizeObserver()) | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | func TestSchedulePopupShellNativeFallback_StartsFallbackOnTimeout(t *testing.T) { | |||
| 210 | + | factory := &WebViewFactory{} | |||
| 211 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true} | |||
| 212 | + | var scheduled func() | |||
| 213 | + | wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer { | |||
| 214 | + | scheduled = fn | |||
| 215 | + | return stubStoppableTimer{} | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | factory.schedulePopupShellNativeFallback(context.Background(), wv) | |||
| 219 | + | require.NotNil(t, scheduled) | |||
| 220 | + | ||||
| 221 | + | scheduled() | |||
| 222 | + | ||||
| 223 | + | require.True(t, wv.nativePopupFallbackStarted) | |||
| 224 | + | require.False(t, wv.nativePopupCandidate) | |||
| 225 | + | } | |||
| 226 | + | ||||
| 58 | func TestPostPendingBrowserCreate_RetriesWhenPostingTaskFails(t *testing.T) { | 227 | func TestPostPendingBrowserCreate_RetriesWhenPostingTaskFails(t *testing.T) { | ||
| 59 | oldNewTask := cefNewTask | 228 | oldNewTask := cefNewTask | ||
| 60 | oldPostTask := cefPostTask | 229 | oldPostTask := cefPostTask | ||
M internal/infrastructure/cef/webview.go +9 -0
| @@ -767,6 +767,15 @@ func (wv *WebView) awaitsNativePopupAttachment() bool { | |||
| 767 | 767 | return wv.browser == nil && !wv.nativePopupFallbackStarted && (wv.nativePopupCandidate || wv.nativePopupID != 0) | |
| 768 | 768 | } | |
| 769 | 769 | ||
| 770 | + | func (wv *WebView) awaitsBrowserCreateFromNativePopupFallback() bool { | |
| 771 | + | if wv == nil { | |
| 772 | + | return false | |
| 773 | + | } | |
| 774 | + | wv.mu.RLock() | |
| 775 | + | defer wv.mu.RUnlock() | |
| 776 | + | return wv.browser == nil && wv.nativePopupFallbackStarted && wv.pendingCreate != nil | |
| 777 | + | } | |
| 778 | + | ||
| 770 | 779 | func (wv *WebView) nativePopupFallbackScheduler() func(time.Duration, func()) stoppableTimer { | |
| 771 | 780 | if wv != nil && wv.nativePopupFallbackSchedule != nil { | |
| 772 | 781 | return wv.nativePopupFallbackSchedule | |
| @@ -767,6 +767,15 @@ func (wv *WebView) awaitsNativePopupAttachment() bool { | |||||
| 767 | return wv.browser == nil && !wv.nativePopupFallbackStarted && (wv.nativePopupCandidate || wv.nativePopupID != 0) | 767 | return wv.browser == nil && !wv.nativePopupFallbackStarted && (wv.nativePopupCandidate || wv.nativePopupID != 0) | ||
| 768 | } | 768 | } | ||
| 769 | 769 | ||||
| 770 | + | func (wv *WebView) awaitsBrowserCreateFromNativePopupFallback() bool { | |||
| 771 | + | if wv == nil { | |||
| 772 | + | return false | |||
| 773 | + | } | |||
| 774 | + | wv.mu.RLock() | |||
| 775 | + | defer wv.mu.RUnlock() | |||
| 776 | + | return wv.browser == nil && wv.nativePopupFallbackStarted && wv.pendingCreate != nil | |||
| 777 | + | } | |||
| 778 | + | ||||
| 770 | func (wv *WebView) nativePopupFallbackScheduler() func(time.Duration, func()) stoppableTimer { | 779 | func (wv *WebView) nativePopupFallbackScheduler() func(time.Duration, func()) stoppableTimer { | ||
| 771 | if wv != nil && wv.nativePopupFallbackSchedule != nil { | 780 | if wv != nil && wv.nativePopupFallbackSchedule != nil { | ||
| 772 | return wv.nativePopupFallbackSchedule | 781 | return wv.nativePopupFallbackSchedule | ||