diff --git a/browser/helpers.go b/browser/helpers.go new file mode 100644 index 000000000..47375281f --- /dev/null +++ b/browser/helpers.go @@ -0,0 +1,43 @@ +package browser + +import ( + "context" + "errors" + + "github.com/dop251/goja" + + "github.com/grafana/xk6-browser/k6error" + "github.com/grafana/xk6-browser/k6ext" +) + +func panicIfFatalError(ctx context.Context, err error) { + if errors.Is(err, k6error.ErrFatal) { + k6ext.Abort(ctx, err.Error()) + } +} + +// exportArg exports the value and returns it. +// It returns nil if the value is undefined or null. +func exportArg(gv goja.Value) any { + if !gojaValueExists(gv) { + return nil + } + return gv.Export() +} + +// exportArgs returns a slice of exported Goja values. +func exportArgs(gargs []goja.Value) []any { + args := make([]any, 0, len(gargs)) + for _, garg := range gargs { + // leaves a nil garg in the array since users might want to + // pass undefined or null as an argument to a function + args = append(args, exportArg(garg)) + } + return args +} + +// gojaValueExists returns true if a given value is not nil and exists +// (defined and not null) in the goja runtime. +func gojaValueExists(v goja.Value) bool { + return v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) +} diff --git a/browser/mapping.go b/browser/mapping.go index 6dca230d3..85b96cf37 100644 --- a/browser/mapping.go +++ b/browser/mapping.go @@ -2,7 +2,6 @@ package browser import ( "context" - "errors" "fmt" "time" @@ -88,7 +87,7 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping { if err := popts.Parse(vu.Context(), opts); err != nil { return fmt.Errorf("parsing locator dispatch event options: %w", err) } - return lo.DispatchEvent(typ, eventInit.Export(), popts) //nolint:wrapcheck + return lo.DispatchEvent(typ, exportArg(eventInit), popts) //nolint:wrapcheck }, "waitFor": lo.WaitFor, } @@ -190,16 +189,12 @@ func mapJSHandle(vu moduleVU, jsh common.JSHandleAPI) mapping { "evaluate": func(pageFunc goja.Value, gargs ...goja.Value) any { args := make([]any, 0, len(gargs)) for _, a := range gargs { - args = append(args, a.Export()) + args = append(args, exportArg(a)) } return jsh.Evaluate(pageFunc.String(), args...) }, "evaluateHandle": func(pageFunc goja.Value, gargs ...goja.Value) (mapping, error) { - args := make([]any, 0, len(gargs)) - for _, a := range gargs { - args = append(args, a.Export()) - } - h, err := jsh.EvaluateHandle(pageFunc.String(), args...) + h, err := jsh.EvaluateHandle(pageFunc.String(), exportArgs(gargs)...) if err != nil { return nil, err //nolint:wrapcheck } @@ -258,7 +253,7 @@ func mapElementHandle(vu moduleVU, eh *common.ElementHandle) mapping { }, "dblclick": eh.Dblclick, "dispatchEvent": func(typ string, eventInit goja.Value) error { - return eh.DispatchEvent(typ, eventInit.Export()) //nolint:wrapcheck + return eh.DispatchEvent(typ, exportArg(eventInit)) //nolint:wrapcheck }, "fill": eh.Fill, "focus": eh.Focus, @@ -387,21 +382,13 @@ func mapFrame(vu moduleVU, f *common.Frame) mapping { if err := popts.Parse(vu.Context(), opts); err != nil { return fmt.Errorf("parsing frame dispatch event options: %w", err) } - return f.DispatchEvent(selector, typ, eventInit.Export(), popts) //nolint:wrapcheck + return f.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck }, "evaluate": func(pageFunction goja.Value, gargs ...goja.Value) any { - args := make([]any, 0, len(gargs)) - for _, a := range gargs { - args = append(args, a.Export()) - } - return f.Evaluate(pageFunction.String(), args...) + return f.Evaluate(pageFunction.String(), exportArgs(gargs)...) }, "evaluateHandle": func(pageFunction goja.Value, gargs ...goja.Value) (mapping, error) { - args := make([]any, 0, len(gargs)) - for _, a := range gargs { - args = append(args, a.Export()) - } - jsh, err := f.EvaluateHandle(pageFunction.String(), args...) + jsh, err := f.EvaluateHandle(pageFunction.String(), exportArgs(gargs)...) if err != nil { return nil, err //nolint:wrapcheck } @@ -535,7 +522,7 @@ func mapFrame(vu moduleVU, f *common.Frame) mapping { } func parseWaitForFunctionArgs( - ctx context.Context, timeout time.Duration, pageFunc, opts goja.Value, args ...goja.Value, + ctx context.Context, timeout time.Duration, pageFunc, opts goja.Value, gargs ...goja.Value, ) (string, *common.FrameWaitForFunctionOptions, []any, error) { popts := common.NewFrameWaitForFunctionOptions(timeout) err := popts.Parse(ctx, opts) @@ -549,12 +536,7 @@ func parseWaitForFunctionArgs( js = fmt.Sprintf("() => (%s)", js) } - pargs := make([]any, 0, len(args)) - for _, arg := range args { - pargs = append(pargs, arg.Export()) - } - - return js, popts, pargs, nil + return js, popts, exportArgs(gargs), nil } // mapPage to the JS module. @@ -592,24 +574,16 @@ func mapPage(vu moduleVU, p *common.Page) mapping { if err := popts.Parse(vu.Context(), opts); err != nil { return fmt.Errorf("parsing page dispatch event options: %w", err) } - return p.DispatchEvent(selector, typ, eventInit.Export(), popts) //nolint:wrapcheck + return p.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck }, "dragAndDrop": p.DragAndDrop, "emulateMedia": p.EmulateMedia, "emulateVisionDeficiency": p.EmulateVisionDeficiency, "evaluate": func(pageFunction goja.Value, gargs ...goja.Value) any { - args := make([]any, 0, len(gargs)) - for _, a := range gargs { - args = append(args, a.Export()) - } - return p.Evaluate(pageFunction.String(), args...) + return p.Evaluate(pageFunction.String(), exportArgs(gargs)...) }, "evaluateHandle": func(pageFunc goja.Value, gargs ...goja.Value) (mapping, error) { - args := make([]any, 0, len(gargs)) - for _, a := range gargs { - args = append(args, a.Export()) - } - jsh, err := p.EvaluateHandle(pageFunc.String(), args...) + jsh, err := p.EvaluateHandle(pageFunc.String(), exportArgs(gargs)...) if err != nil { return nil, err //nolint:wrapcheck } @@ -1036,9 +1010,3 @@ func mapBrowser(vu moduleVU) mapping { //nolint:funlen }, } } - -func panicIfFatalError(ctx context.Context, err error) { - if errors.Is(err, k6error.ErrFatal) { - k6ext.Abort(ctx, err.Error()) - } -} diff --git a/examples/dispatch.js b/examples/dispatch.js new file mode 100644 index 000000000..5414eae8c --- /dev/null +++ b/examples/dispatch.js @@ -0,0 +1,36 @@ +import { check } from 'k6'; +import { browser } from 'k6/x/browser'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ["rate==1.0"] + } +} + +export default async function() { + const context = browser.newContext(); + const page = context.newPage(); + + try { + await page.goto('https://test.k6.io/', { waitUntil: 'networkidle' }); + + page.locator('a[href="/contacts.php"]') + .dispatchEvent("click"); + + check(page, { + header: (p) => p.locator("h3").textContent() == "Contact us", + }); + } finally { + page.close(); + } +} diff --git a/examples/evaluate.js b/examples/evaluate.js index 0592dbedf..ec0f0326c 100644 --- a/examples/evaluate.js +++ b/examples/evaluate.js @@ -22,15 +22,23 @@ export default async function() { const page = context.newPage(); try { - await page.goto('https://test.k6.io/', { waitUntil: 'load' }); - - const result = page.evaluate(([x, y]) => { - return Promise.resolve(x * y); - }, [5, 5]); - console.log(result); // tests #120 - + await page.goto("https://test.k6.io/", { waitUntil: "load" }); + + // calling evaluate without arguments + let result = page.evaluate(() => { + return Promise.resolve(5 * 42); + }); + check(result, { + "result should be 210": (result) => result == 210, + }); + + // calling evaluate with arguments + result = page.evaluate(([x, y]) => { + return Promise.resolve(x * y); + }, [5, 5] + ); check(result, { - 'result is 25': (result) => result == 25, + "result should be 25": (result) => result == 25, }); } finally { page.close();