feat(chromium): add 'waitForExpression' form field, deprecate 'waitWindowStatus' form field

This commit is contained in:
Julien Neuhart
2021-11-21 19:50:53 +01:00
parent 5b8391067d
commit ed47493459
4 changed files with 134 additions and 27 deletions

View File

@@ -30,6 +30,10 @@ var (
// allowed/denied lists.
ErrURLNotAuthorized = errors.New("URL not authorized")
// ErrInvalidEvaluationExpression happens if an evaluation expression
// returns an exception or undefined.
ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression")
// ErrInvalidPrinterSettings happens if the Options have one or more
// aberrant values.
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
@@ -71,6 +75,11 @@ type Options struct {
// Optional.
WaitWindowStatus string
// WaitForExpression is the custom JavaScript expression to wait before
// converting an HTML document to PDF until it returns true
// Optional.
WaitForExpression string
// UserAgent overrides the default User-Agent header.
// Optional.
UserAgent string
@@ -149,6 +158,7 @@ func DefaultOptions() Options {
return Options{
WaitDelay: 0,
WaitWindowStatus: "",
WaitForExpression: "",
UserAgent: "",
ExtraHTTPHeaders: nil,
Landscape: false,
@@ -454,42 +464,61 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
return nil
}),
chromedp.ActionFunc(func(ctx context.Context) error {
if options.WaitWindowStatus == "" {
if options.WaitWindowStatus == "" && options.WaitForExpression == "" {
return nil
}
// We wait until the evaluation of
// "window.status === options.WaitWindowStatus" is true or
// until the context is done.
logger.Debug(fmt.Sprintf("wait for window.status === '%s' before print", options.WaitWindowStatus))
evaluate := func(expression string) error {
// We wait until the evaluation of the expression is true or
// until the context is done.
logger.Debug(fmt.Sprintf("wait until '%s' is true before print", expression))
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
for {
select {
case <-ctx.Done():
ticker.Stop()
return fmt.Errorf("wait for window.status === '%s': %w", options.WaitWindowStatus, ctx.Err())
case <-ticker.C:
var ok bool
evaluate := chromedp.Evaluate(fmt.Sprintf("window.status === '%s'", options.WaitWindowStatus), &ok)
err := evaluate.Do(ctx)
if err != nil {
return fmt.Errorf("evaluate: %w", err)
}
if ok {
for {
select {
case <-ctx.Done():
ticker.Stop()
return nil
}
return fmt.Errorf("context done while evaluating '%s': %w", expression, ctx.Err())
case <-ticker.C:
var ok bool
continue
evaluate := chromedp.Evaluate(expression, &ok)
err := evaluate.Do(ctx)
if err != nil {
return fmt.Errorf("evaluate: %v: %w", err, ErrInvalidEvaluationExpression)
}
if ok {
ticker.Stop()
return nil
}
continue
}
}
}
if options.WaitWindowStatus != "" {
logger.Warn("option 'WaitWindowStatus' is deprecated; prefer 'WaitForExpression' instead")
err := evaluate(fmt.Sprintf("window.status === '%s'", options.WaitWindowStatus))
if err != nil {
return fmt.Errorf("wait for window.status === '%s': %w", options.WaitWindowStatus, err)
}
}
if options.WaitForExpression != "" {
err := evaluate(options.WaitForExpression)
if err != nil {
return fmt.Errorf("wait for expression '%s': %w", options.WaitForExpression, err)
}
}
return nil
}),
chromedp.ActionFunc(func(ctx context.Context) error {
printToPDF := page.PrintToPDF().

View File

@@ -285,6 +285,21 @@ func TestChromium_PDF(t *testing.T) {
WaitWindowStatus: "ready",
},
},
{
timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{
WaitForExpression: "window.status === 'foo'",
},
expectErr: true,
},
{
timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{
WaitForExpression: "window.status === 'ready'",
},
},
{
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{

View File

@@ -28,6 +28,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
var (
waitDelay time.Duration
waitWindowStatus string
waitForExpression string
userAgent string
extraHTTPHeaders map[string]string
landscape, printBackground bool
@@ -41,6 +42,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
form := ctx.FormData().
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
String("userAgent", &userAgent, defaultOptions.UserAgent).
Custom("extraHttpHeaders", func(value string) error {
if value == "" {
@@ -73,6 +75,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
options := Options{
WaitDelay: waitDelay,
WaitWindowStatus: waitWindowStatus,
WaitForExpression: waitForExpression,
UserAgent: userAgent,
ExtraHTTPHeaders: extraHTTPHeaders,
Landscape: landscape,
@@ -291,6 +294,23 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL,
)
}
if errors.Is(err, ErrInvalidEvaluationExpression) {
if options.WaitForExpression == "" {
// We do not expect the 'waitWindowStatus' form field to return
// an ErrInvalidEvaluationExpression error. In such a scenario,
// we return a 500.
return fmt.Errorf("convert to PDF: %w", err)
}
return api.WrapError(
fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError(
http.StatusBadRequest,
fmt.Sprintf("The expression '%s' (waitForExpression) returned an exception or undefined", options.WaitForExpression),
),
)
}
if errors.Is(err, ErrInvalidPrinterSettings) {
return api.WrapError(
fmt.Errorf("convert to PDF: %w", err),

View File

@@ -523,6 +523,7 @@ func TestConvertURL(t *testing.T) {
api API
engine gotenberg.PDFEngine
PDFformat string
options Options
expectErr bool
expectHTTPErr bool
expectHTTPStatus int
@@ -538,10 +539,44 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI
}(),
options: DefaultOptions(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusForbidden,
},
{
ctx: &api.MockContext{Context: &api.Context{}},
api: func() API {
chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
return ErrInvalidEvaluationExpression
}
return chromiumAPI
}(),
options: DefaultOptions(),
expectErr: true,
},
{
ctx: &api.MockContext{Context: &api.Context{}},
api: func() API {
chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
return ErrInvalidEvaluationExpression
}
return chromiumAPI
}(),
options: func() Options {
options := DefaultOptions()
options.WaitForExpression = "foo"
return options
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
ctx: &api.MockContext{Context: &api.Context{}},
api: func() API {
@@ -552,6 +587,7 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI
}(),
options: DefaultOptions(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
@@ -566,6 +602,7 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI
}(),
options: DefaultOptions(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
@@ -580,6 +617,7 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI
}(),
options: DefaultOptions(),
expectErr: true,
},
{
@@ -600,6 +638,7 @@ func TestConvertURL(t *testing.T) {
}
}(),
PDFformat: "foo",
options: DefaultOptions(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
@@ -622,6 +661,7 @@ func TestConvertURL(t *testing.T) {
}
}(),
PDFformat: "foo",
options: DefaultOptions(),
expectErr: true,
},
{
@@ -642,6 +682,7 @@ func TestConvertURL(t *testing.T) {
}
}(),
PDFformat: "foo",
options: DefaultOptions(),
expectOutputPathsCount: 1,
},
{
@@ -659,6 +700,7 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI
}(),
options: DefaultOptions(),
expectErr: true,
},
{
@@ -671,10 +713,11 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI
}(),
options: DefaultOptions(),
expectOutputPathsCount: 1,
},
} {
err := convertURL(tc.ctx.Context, tc.api, tc.engine, "", tc.PDFformat, DefaultOptions())
err := convertURL(tc.ctx.Context, tc.api, tc.engine, "", tc.PDFformat, tc.options)
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)