From 200334197f664cbe3307cf291ac2fda11b4612b0 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Fri, 24 Apr 2026 14:30:45 +0200 Subject: [PATCH] feat(chromium): always inject paint-callback polyfill to cover waitDelay users --- pkg/modules/chromium/browser.go | 6 ++-- pkg/modules/chromium/paint_polyfill.go | 15 +++++----- .../features/chromium_convert_html.feature | 30 ++++++++++++++----- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/pkg/modules/chromium/browser.go b/pkg/modules/chromium/browser.go index c97890a4..5005da98 100644 --- a/pkg/modules/chromium/browser.go +++ b/pkg/modules/chromium/browser.go @@ -308,7 +308,6 @@ func (b *chromiumBrowser) Healthy(logger *slog.Logger) bool { func (b *chromiumBrowser) pdf(ctx context.Context, logger *slog.Logger, url, outputPath string, options PdfOptions) error { // Note: no error wrapping because it leaks on errors we want to display to // the end user. - installPaintPolyfill := options.WaitForExpression != "" || options.WaitForSelector != "" return b.do(ctx, logger, url, options.Options, chromedp.Tasks{ network.Enable(), fetch.Enable(), @@ -318,7 +317,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *slog.Logger, url, out disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), setCookiesActionFunc(logger, options.Cookies), userAgentOverride(logger, options.UserAgent), - injectPaintCallbacksPolyfillActionFunc(logger, installPaintPolyfill), + injectPaintCallbacksPolyfillActionFunc(logger), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground), forceExactColorsActionFunc(logger, options.PrintBackground), @@ -336,7 +335,6 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *slog.Logger, url, out func (b *chromiumBrowser) screenshot(ctx context.Context, logger *slog.Logger, url, outputPath string, options ScreenshotOptions) error { // Note: no error wrapping because it leaks on errors we want to display to // the end user. - installPaintPolyfill := options.WaitForExpression != "" || options.WaitForSelector != "" return b.do(ctx, logger, url, options.Options, chromedp.Tasks{ network.Enable(), fetch.Enable(), @@ -346,7 +344,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *slog.Logger, u disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), setCookiesActionFunc(logger, options.Cookies), userAgentOverride(logger, options.UserAgent), - injectPaintCallbacksPolyfillActionFunc(logger, installPaintPolyfill), + injectPaintCallbacksPolyfillActionFunc(logger), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true), forceExactColorsActionFunc(logger, true), diff --git a/pkg/modules/chromium/paint_polyfill.go b/pkg/modules/chromium/paint_polyfill.go index 9863fc30..9d7a9e5f 100644 --- a/pkg/modules/chromium/paint_polyfill.go +++ b/pkg/modules/chromium/paint_polyfill.go @@ -124,15 +124,14 @@ const paintCallbacksPolyfill = ` // injectPaintCallbacksPolyfillActionFunc installs the // [paintCallbacksPolyfill] via [page.AddScriptToEvaluateOnNewDocument] -// so that it runs before any user script on the navigated page. When -// install is false the action is a no-op and the page keeps the native -// rAF, ResizeObserver, and IntersectionObserver implementations. -func injectPaintCallbacksPolyfillActionFunc(logger *slog.Logger, install bool) chromedp.ActionFunc { +// so that it runs before any user script on the navigated page. The +// shim is installed unconditionally: the native rAF, ResizeObserver, +// and IntersectionObserver APIs do not fire during Chromium's +// headless-print render, so replacing them with timer-backed +// implementations either restores the behavior the page expects or has +// no effect when the page never calls them. +func injectPaintCallbacksPolyfillActionFunc(logger *slog.Logger) chromedp.ActionFunc { return func(ctx context.Context) error { - if !install { - logger.DebugContext(ctx, "paint-callbacks polyfill not requested") - return nil - } logger.DebugContext(ctx, "inject paint-callbacks polyfill") _, err := page.AddScriptToEvaluateOnNewDocument(paintCallbacksPolyfill).Do(ctx) if err != nil { diff --git a/test/integration/features/chromium_convert_html.feature b/test/integration/features/chromium_convert_html.feature index 11b0e519..8b86610e 100644 --- a/test/integration/features/chromium_convert_html.feature +++ b/test/integration/features/chromium_convert_html.feature @@ -195,7 +195,7 @@ Feature: /forms/chromium/convert/html Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. """ - Scenario: POST /forms/chromium/convert/html (paint-callback polyfill fires rAF / ResizeObserver / IntersectionObserver when waitForExpression is set) + Scenario: POST /forms/chromium/convert/html (paint-callback polyfill fires rAF / ResizeObserver / IntersectionObserver with waitForExpression) Given I have a default Gotenberg container When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): | files | testdata/paint-callbacks-html/index.html | file | @@ -219,14 +219,30 @@ Feature: /forms/chromium/convert/html io-fired """ - Scenario: POST /forms/chromium/convert/html (paint-callback polyfill skipped without a readiness signal) - Given I have a Gotenberg container with the following environment variable(s): - | LOG_LEVEL | debug | + Scenario: POST /forms/chromium/convert/html (paint-callback polyfill fires rAF / ResizeObserver / IntersectionObserver with waitDelay and emulatedMediaType=print) + Given I have a default Gotenberg container When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): - | files | testdata/page-1-html/index.html | file | + | files | testdata/paint-callbacks-html/index.html | file | + | waitDelay | 3s | field | + | emulatedMediaType | print | field | + | Gotenberg-Output-Filename | foo | header | Then the response status code should be 200 - Then the Gotenberg container should log the following entries: - | paint-callbacks polyfill not requested | + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have the following content at page 1: + """ + raf-fired + """ + Then the "foo.pdf" PDF should have the following content at page 1: + """ + ro-fired + """ + Then the "foo.pdf" PDF should have the following content at page 1: + """ + io-fired + """ Scenario: POST /forms/chromium/convert/html (Wait For Selector) Given I have a default Gotenberg container