feat(chromium): add waitForSelector option to Chromium conversions (#1446)

* Add `waitForSelector` option to Chromium conversions

Closes #960

As an alternative to waiting on an expression, this allows users to wait
for a specific node matching a selector to become visible in the HTML /
at the remote URL before converting to PDF.

* Fix style / prettify
This commit is contained in:
Daniel Moran
2026-01-17 05:57:10 -08:00
committed by GitHub
parent 92de0cf6fe
commit 3220ca4140
8 changed files with 122 additions and 2 deletions
+2
View File
@@ -290,6 +290,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
forceExactColorsActionFunc(logger, options.PrintBackground),
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
// PDF specific.
printToPdfActionFunc(logger, outputPath, options),
@@ -315,6 +316,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
forceExactColorsActionFunc(logger, true),
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
// Screenshot specific.
setDeviceMetricsOverride(logger, options.Width, options.Height),
+9
View File
@@ -33,6 +33,10 @@ var (
// returns an exception or undefined.
ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression")
// ErrInvalidSelectorQuery happens if a selector query returns an exception
// or undefined.
ErrInvalidSelectorQuery = errors.New("invalid selector query")
// ErrRpccMessageTooLarge happens when the messages received by
// ChromeDevTools are larger than 100 MB.
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
@@ -142,6 +146,10 @@ type Options struct {
// converting an HTML document until it returns true
WaitForExpression string
// WaitForSelector is the element query to wait until visible before
// converting an HTML document.
WaitForSelector string
// Cookies are the cookies to put in the Chromium cookies' jar.
Cookies []Cookie
@@ -173,6 +181,7 @@ func DefaultOptions() Options {
WaitDelay: 0,
WaitWindowStatus: "",
WaitForExpression: "",
WaitForSelector: "",
Cookies: nil,
UserAgent: "",
ExtraHttpHeaders: nil,
+19
View File
@@ -55,6 +55,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
waitDelay time.Duration
waitWindowStatus string
waitForExpression string
waitForSelector string
cookies []Cookie
userAgent string
extraHttpHeaders []ExtraHttpHeader
@@ -108,6 +109,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
String("waitForSelector", &waitForSelector, defaultOptions.WaitForSelector).
Custom("cookies", func(value string) error {
if value == "" {
cookies = defaultOptions.Cookies
@@ -237,6 +239,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
WaitDelay: waitDelay,
WaitWindowStatus: waitWindowStatus,
WaitForExpression: waitForExpression,
WaitForSelector: waitForSelector,
Cookies: cookies,
UserAgent: userAgent,
ExtraHttpHeaders: extraHttpHeaders,
@@ -809,6 +812,22 @@ func handleChromiumError(err error, options Options) error {
)
}
if errors.Is(err, ErrInvalidSelectorQuery) {
if options.WaitForSelector == "" {
// We only expect to see this error if the user specified a selector.
// If they didn't and we still generated the error, return a 500.
return err
}
return api.WrapError(
err,
api.NewSentinelHttpError(
http.StatusBadRequest,
fmt.Sprintf("The selector '%s' (waitForSelector) returned an exception or undefined", options.WaitForSelector),
),
)
}
if errors.Is(err, ErrInvalidHttpStatusCode) {
return api.WrapError(
err,
+16
View File
@@ -512,3 +512,19 @@ func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJavaScrip
}
}
}
func waitForSelectorVisibleBeforePrintActionFunc(logger *zap.Logger, selector string) chromedp.ActionFunc {
return func(ctx context.Context) error {
if selector == "" {
logger.Debug("no wait selector")
return nil
}
logger.Debug(fmt.Sprintf("wait until '%s' is visible before print", selector))
err := chromedp.WaitVisible(selector, chromedp.ByQuery, chromedp.RetryInterval(time.Duration(100)*time.Millisecond)).Do(ctx)
if err != nil {
return fmt.Errorf("wait visible: %v: %w", err, ErrInvalidSelectorQuery)
}
return nil
}
}
@@ -195,6 +195,36 @@ Feature: /forms/chromium/convert/html
Wait delay > 2 seconds or expression window globalVar === 'ready' returns true.
"""
Scenario: POST /forms/chromium/convert/html (Wait For Selector)
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/feature-rich-html/index.html | file |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
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 1 page(s)
Then the "foo.pdf" PDF should NOT have the following content at page 1:
"""
Wait on selector returns true.
"""
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/feature-rich-html/index.html | file |
| waitForSelector | #wait-selector | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
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 1 page(s)
Then the "foo.pdf" PDF should have the following content at page 1:
"""
Wait on selector returns true.
"""
Scenario: POST /forms/chromium/convert/html (Emulated Media Type)
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):
@@ -255,6 +255,38 @@ Feature: /forms/chromium/convert/url
Wait delay > 2 seconds or expression window globalVar === 'ready' returns true.
"""
Scenario: POST /forms/chromium/convert/url (Wait For Selector)
Given I have a default Gotenberg container
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
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 1 page(s)
Then the "foo.pdf" PDF should NOT have the following content at page 1:
"""
Wait on selector returns true.
"""
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field |
| waitForSelector | #wait-selector | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
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 1 page(s)
Then the "foo.pdf" PDF should have the following content at page 1:
"""
Wait on selector returns true.
"""
Scenario: POST /forms/chromium/convert/url (Emulated Media Type)
Given I have a default Gotenberg container
Given I have a static server
@@ -37,8 +37,14 @@
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
delay(2000).then(() => {
document.getElementById("wait").style.display = "contents";
const waitText = document.getElementById("wait");
waitText.style.display = "contents";
window.globalVar = "ready";
const newText = document.createElement("p");
newText.id = "wait-selector";
newText.textContent = "Wait on selector returns true.";
waitText.parentNode.insertBefore(newText, waitText);
});
</script>
+7 -1
View File
@@ -46,8 +46,14 @@
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
delay(2000).then(() => {
document.getElementById("wait").style.display = "contents";
const waitText = document.getElementById("wait");
waitText.style.display = "contents";
window.globalVar = "ready";
const newText = document.createElement("p");
newText.id = "wait-selector";
newText.textContent = "Wait on selector returns true.";
waitText.parentNode.insertBefore(newText, waitText);
});
</script>