feat(chromium): always inject paint-callback polyfill to cover waitDelay users

This commit is contained in:
Julien Neuhart
2026-04-24 14:30:45 +02:00
parent ce9c48b2b0
commit 200334197f
3 changed files with 32 additions and 19 deletions

View File

@@ -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 { 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 // Note: no error wrapping because it leaks on errors we want to display to
// the end user. // the end user.
installPaintPolyfill := options.WaitForExpression != "" || options.WaitForSelector != ""
return b.do(ctx, logger, url, options.Options, chromedp.Tasks{ return b.do(ctx, logger, url, options.Options, chromedp.Tasks{
network.Enable(), network.Enable(),
fetch.Enable(), fetch.Enable(),
@@ -318,7 +317,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *slog.Logger, url, out
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies), setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent), userAgentOverride(logger, options.UserAgent),
injectPaintCallbacksPolyfillActionFunc(logger, installPaintPolyfill), injectPaintCallbacksPolyfillActionFunc(logger),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
forceExactColorsActionFunc(logger, 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 { 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 // Note: no error wrapping because it leaks on errors we want to display to
// the end user. // the end user.
installPaintPolyfill := options.WaitForExpression != "" || options.WaitForSelector != ""
return b.do(ctx, logger, url, options.Options, chromedp.Tasks{ return b.do(ctx, logger, url, options.Options, chromedp.Tasks{
network.Enable(), network.Enable(),
fetch.Enable(), fetch.Enable(),
@@ -346,7 +344,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *slog.Logger, u
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies), setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent), userAgentOverride(logger, options.UserAgent),
injectPaintCallbacksPolyfillActionFunc(logger, installPaintPolyfill), injectPaintCallbacksPolyfillActionFunc(logger),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
forceExactColorsActionFunc(logger, true), forceExactColorsActionFunc(logger, true),

View File

@@ -124,15 +124,14 @@ const paintCallbacksPolyfill = `
// injectPaintCallbacksPolyfillActionFunc installs the // injectPaintCallbacksPolyfillActionFunc installs the
// [paintCallbacksPolyfill] via [page.AddScriptToEvaluateOnNewDocument] // [paintCallbacksPolyfill] via [page.AddScriptToEvaluateOnNewDocument]
// so that it runs before any user script on the navigated page. When // so that it runs before any user script on the navigated page. The
// install is false the action is a no-op and the page keeps the native // shim is installed unconditionally: the native rAF, ResizeObserver,
// rAF, ResizeObserver, and IntersectionObserver implementations. // and IntersectionObserver APIs do not fire during Chromium's
func injectPaintCallbacksPolyfillActionFunc(logger *slog.Logger, install bool) chromedp.ActionFunc { // 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 { 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") logger.DebugContext(ctx, "inject paint-callbacks polyfill")
_, err := page.AddScriptToEvaluateOnNewDocument(paintCallbacksPolyfill).Do(ctx) _, err := page.AddScriptToEvaluateOnNewDocument(paintCallbacksPolyfill).Do(ctx)
if err != nil { if err != nil {

View File

@@ -195,7 +195,7 @@ Feature: /forms/chromium/convert/html
Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. 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 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): 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 | | files | testdata/paint-callbacks-html/index.html | file |
@@ -219,14 +219,30 @@ Feature: /forms/chromium/convert/html
io-fired io-fired
""" """
Scenario: POST /forms/chromium/convert/html (paint-callback polyfill skipped without a readiness signal) Scenario: POST /forms/chromium/convert/html (paint-callback polyfill fires rAF / ResizeObserver / IntersectionObserver with waitDelay and emulatedMediaType=print)
Given I have a Gotenberg container with the following environment variable(s): Given I have a default Gotenberg container
| LOG_LEVEL | debug |
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): 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 response status code should be 200
Then the Gotenberg container should log the following entries: Then the response header "Content-Type" should be "application/pdf"
| paint-callbacks polyfill not requested | 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) Scenario: POST /forms/chromium/convert/html (Wait For Selector)
Given I have a default Gotenberg container Given I have a default Gotenberg container