mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-16 20:32:13 +01:00
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:
@@ -290,6 +290,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
|||||||
forceExactColorsActionFunc(logger, options.PrintBackground),
|
forceExactColorsActionFunc(logger, options.PrintBackground),
|
||||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||||
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||||
|
waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector),
|
||||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||||
// PDF specific.
|
// PDF specific.
|
||||||
printToPdfActionFunc(logger, outputPath, options),
|
printToPdfActionFunc(logger, outputPath, options),
|
||||||
@@ -315,6 +316,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
|
|||||||
forceExactColorsActionFunc(logger, true),
|
forceExactColorsActionFunc(logger, true),
|
||||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||||
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||||
|
waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector),
|
||||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||||
// Screenshot specific.
|
// Screenshot specific.
|
||||||
setDeviceMetricsOverride(logger, options.Width, options.Height),
|
setDeviceMetricsOverride(logger, options.Width, options.Height),
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ var (
|
|||||||
// returns an exception or undefined.
|
// returns an exception or undefined.
|
||||||
ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression")
|
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
|
// ErrRpccMessageTooLarge happens when the messages received by
|
||||||
// ChromeDevTools are larger than 100 MB.
|
// ChromeDevTools are larger than 100 MB.
|
||||||
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
|
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
|
||||||
@@ -142,6 +146,10 @@ type Options struct {
|
|||||||
// converting an HTML document until it returns true
|
// converting an HTML document until it returns true
|
||||||
WaitForExpression string
|
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 are the cookies to put in the Chromium cookies' jar.
|
||||||
Cookies []Cookie
|
Cookies []Cookie
|
||||||
|
|
||||||
@@ -173,6 +181,7 @@ func DefaultOptions() Options {
|
|||||||
WaitDelay: 0,
|
WaitDelay: 0,
|
||||||
WaitWindowStatus: "",
|
WaitWindowStatus: "",
|
||||||
WaitForExpression: "",
|
WaitForExpression: "",
|
||||||
|
WaitForSelector: "",
|
||||||
Cookies: nil,
|
Cookies: nil,
|
||||||
UserAgent: "",
|
UserAgent: "",
|
||||||
ExtraHttpHeaders: nil,
|
ExtraHttpHeaders: nil,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
waitDelay time.Duration
|
waitDelay time.Duration
|
||||||
waitWindowStatus string
|
waitWindowStatus string
|
||||||
waitForExpression string
|
waitForExpression string
|
||||||
|
waitForSelector string
|
||||||
cookies []Cookie
|
cookies []Cookie
|
||||||
userAgent string
|
userAgent string
|
||||||
extraHttpHeaders []ExtraHttpHeader
|
extraHttpHeaders []ExtraHttpHeader
|
||||||
@@ -108,6 +109,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
||||||
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
||||||
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
||||||
|
String("waitForSelector", &waitForSelector, defaultOptions.WaitForSelector).
|
||||||
Custom("cookies", func(value string) error {
|
Custom("cookies", func(value string) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
cookies = defaultOptions.Cookies
|
cookies = defaultOptions.Cookies
|
||||||
@@ -237,6 +239,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
WaitDelay: waitDelay,
|
WaitDelay: waitDelay,
|
||||||
WaitWindowStatus: waitWindowStatus,
|
WaitWindowStatus: waitWindowStatus,
|
||||||
WaitForExpression: waitForExpression,
|
WaitForExpression: waitForExpression,
|
||||||
|
WaitForSelector: waitForSelector,
|
||||||
Cookies: cookies,
|
Cookies: cookies,
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
ExtraHttpHeaders: extraHttpHeaders,
|
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) {
|
if errors.Is(err, ErrInvalidHttpStatusCode) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
err,
|
err,
|
||||||
|
|||||||
@@ -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.
|
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)
|
Scenario: POST /forms/chromium/convert/html (Emulated Media Type)
|
||||||
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):
|
||||||
|
|||||||
@@ -255,6 +255,38 @@ Feature: /forms/chromium/convert/url
|
|||||||
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/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)
|
Scenario: POST /forms/chromium/convert/url (Emulated Media Type)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
Given I have a static server
|
Given I have a static server
|
||||||
|
|||||||
@@ -37,8 +37,14 @@
|
|||||||
|
|
||||||
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||||
delay(2000).then(() => {
|
delay(2000).then(() => {
|
||||||
document.getElementById("wait").style.display = "contents";
|
const waitText = document.getElementById("wait");
|
||||||
|
waitText.style.display = "contents";
|
||||||
window.globalVar = "ready";
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,14 @@
|
|||||||
|
|
||||||
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||||
delay(2000).then(() => {
|
delay(2000).then(() => {
|
||||||
document.getElementById("wait").style.display = "contents";
|
const waitText = document.getElementById("wait");
|
||||||
|
waitText.style.display = "contents";
|
||||||
window.globalVar = "ready";
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user