diff --git a/go.mod b/go.mod index 8c04ef013cb..14a2d1df21f 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/golang/protobuf v1.5.4 github.com/gorilla/websocket v1.5.1 github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78 - github.com/grafana/xk6-browser v1.5.1 + github.com/grafana/xk6-browser v1.5.2-0.20240607140836-ffcc1f5169ad github.com/grafana/xk6-dashboard v0.7.4 github.com/grafana/xk6-output-prometheus-remote v0.3.1 github.com/grafana/xk6-redis v0.2.0 diff --git a/go.sum b/go.sum index 6e76b3887c4..e197a3286dc 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,8 @@ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78 h1:rVCZdB+13G+aQoGm3CBVaDGl0uxZxfjvQgEJy4IeHTA= github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78/go.mod h1:6ZH0b0iOxyigeTh+/IlGoL0Hd3lVXA94xoXf0ldNgCM= -github.com/grafana/xk6-browser v1.5.1 h1:wexnBtx1raDniYcXkRQ9zfXvuJGjvixZag4kmiYG3tg= -github.com/grafana/xk6-browser v1.5.1/go.mod h1:hD9H1zpe1Fvs6RCENKnaPqpObh6alz+hX00Xf5qvDE4= +github.com/grafana/xk6-browser v1.5.2-0.20240607140836-ffcc1f5169ad h1:q3sB942oYrD7NlcsS9hz26I9W+EKfpKVmhKe7dWUp3s= +github.com/grafana/xk6-browser v1.5.2-0.20240607140836-ffcc1f5169ad/go.mod h1:xLaGGhTMHIRsMvkVWFYh9RPy87kG2n4L4Or6DeI8U+o= github.com/grafana/xk6-dashboard v0.7.4 h1:0ZRPTAXW+6A3Xqq/a/OaIZhxUt1SOMwUFff0IPwBHrs= github.com/grafana/xk6-dashboard v0.7.4/go.mod h1:300QyQ+OQAYz/L/AzB5tKzPeBY5eKh2wl1NsRmCbsx4= github.com/grafana/xk6-output-prometheus-remote v0.3.1 h1:X23rQzlJD8dXWB31DkxR4uPnuRFo8L0Y0H22fSG9xl0= diff --git a/vendor/github.com/grafana/xk6-browser/browser/browser_context_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/browser_context_mapping.go new file mode 100644 index 00000000000..f4d14fc2943 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/browser_context_mapping.go @@ -0,0 +1,173 @@ +package browser + +import ( + "fmt" + "reflect" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6error" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapBrowserContext to the JS module. +func mapBrowserContext(vu moduleVU, bc *common.BrowserContext) mapping { //nolint:funlen,gocognit,cyclop + rt := vu.Runtime() + return mapping{ + "addCookies": func(cookies []*common.Cookie) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, bc.AddCookies(cookies) //nolint:wrapcheck + }) + }, + "addInitScript": func(script sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + if !sobekValueExists(script) { + return nil, nil + } + + source := "" + switch script.ExportType() { + case reflect.TypeOf(string("")): + source = script.String() + case reflect.TypeOf(sobek.Object{}): + opts := script.ToObject(rt) + for _, k := range opts.Keys() { + if k == "content" { + source = opts.Get(k).String() + } + } + default: + _, isCallable := sobek.AssertFunction(script) + if !isCallable { + source = fmt.Sprintf("(%s);", script.ToString().String()) + } else { + source = fmt.Sprintf("(%s)(...args);", script.ToString().String()) + } + } + + return nil, bc.AddInitScript(source) //nolint:wrapcheck + }) + }, + "browser": func() mapping { + // the browser is grabbed from VU. + return mapBrowser(vu) + }, + "clearCookies": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, bc.ClearCookies() //nolint:wrapcheck + }) + }, + "clearPermissions": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, bc.ClearPermissions() //nolint:wrapcheck + }) + }, + "close": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, bc.Close() //nolint:wrapcheck + }) + }, + "cookies": func(urls ...string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return bc.Cookies(urls...) //nolint:wrapcheck + }) + }, + "grantPermissions": func(permissions []string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + popts := common.NewGrantPermissionsOptions() + popts.Parse(vu.Context(), opts) + + return nil, bc.GrantPermissions(permissions, popts) //nolint:wrapcheck + }) + }, + "setDefaultNavigationTimeout": bc.SetDefaultNavigationTimeout, + "setDefaultTimeout": bc.SetDefaultTimeout, + "setGeolocation": func(geolocation sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, bc.SetGeolocation(geolocation) //nolint:wrapcheck + }) + }, + "setHTTPCredentials": func(httpCredentials sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, bc.SetHTTPCredentials(httpCredentials) //nolint:staticcheck,wrapcheck + }) + }, + "setOffline": func(offline bool) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, bc.SetOffline(offline) //nolint:wrapcheck + }) + }, + "waitForEvent": func(event string, optsOrPredicate sobek.Value) (*sobek.Promise, error) { + ctx := vu.Context() + popts := common.NewWaitForEventOptions( + bc.Timeout(), + ) + if err := popts.Parse(ctx, optsOrPredicate); err != nil { + return nil, fmt.Errorf("parsing waitForEvent options: %w", err) + } + + return k6ext.Promise(ctx, func() (result any, reason error) { + var runInTaskQueue func(p *common.Page) (bool, error) + if popts.PredicateFn != nil { + runInTaskQueue = func(p *common.Page) (bool, error) { + tq := vu.taskQueueRegistry.get(p.TargetID()) + + var rtn bool + var err error + // The function on the taskqueue runs in its own goroutine + // so we need to use a channel to wait for it to complete + // before returning the result to the caller. + c := make(chan bool) + tq.Queue(func() error { + var resp sobek.Value + resp, err = popts.PredicateFn(vu.Runtime().ToValue(p)) + rtn = resp.ToBoolean() + close(c) + return nil + }) + <-c + + return rtn, err //nolint:wrapcheck + } + } + + resp, err := bc.WaitForEvent(event, runInTaskQueue, popts.Timeout) + panicIfFatalError(ctx, err) + if err != nil { + return nil, err //nolint:wrapcheck + } + p, ok := resp.(*common.Page) + if !ok { + panicIfFatalError(ctx, fmt.Errorf("response object is not a page: %w", k6error.ErrFatal)) + } + + return mapPage(vu, p), nil + }), nil + }, + "pages": func() *sobek.Object { + var ( + mpages []mapping + pages = bc.Pages() + ) + for _, page := range pages { + if page == nil { + continue + } + m := mapPage(vu, page) + mpages = append(mpages, m) + } + + return rt.ToValue(mpages).ToObject(rt) + }, + "newPage": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + page, err := bc.NewPage() + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapPage(vu, page), nil + }) + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/browser_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/browser_mapping.go new file mode 100644 index 00000000000..bfab5190b31 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/browser_mapping.go @@ -0,0 +1,105 @@ +package browser + +import ( + "fmt" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapBrowser to the JS module. +func mapBrowser(vu moduleVU) mapping { //nolint:funlen,cyclop + return mapping{ + "context": func() (mapping, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } + return mapBrowserContext(vu, b.Context()), nil + }, + "closeContext": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } + return nil, b.CloseContext() //nolint:wrapcheck + }) + }, + "isConnected": func() (bool, error) { + b, err := vu.browser() + if err != nil { + return false, err + } + return b.IsConnected(), nil + }, + "newContext": func(opts sobek.Value) (*sobek.Promise, error) { + return k6ext.Promise(vu.Context(), func() (any, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } + bctx, err := b.NewContext(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + if err := initBrowserContext(bctx, vu.testRunID); err != nil { + return nil, err + } + + m := mapBrowserContext(vu, bctx) + + return m, nil + }), nil + }, + "userAgent": func() (string, error) { + b, err := vu.browser() + if err != nil { + return "", err + } + return b.UserAgent(), nil + }, + "version": func() (string, error) { + b, err := vu.browser() + if err != nil { + return "", err + } + return b.Version(), nil + }, + "newPage": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } + page, err := b.NewPage(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + if err := initBrowserContext(b.Context(), vu.testRunID); err != nil { + return nil, err + } + + return mapPage(vu, page), nil + }) + }, + } +} + +func initBrowserContext(bctx *common.BrowserContext, testRunID string) error { + // Setting a k6 object which will contain k6 specific metadata + // on the current test run. This allows external applications + // (such as Grafana Faro) to identify that the session is a k6 + // automated one and not one driven by a real person. + if err := bctx.AddInitScript( + fmt.Sprintf(`window.k6 = { testRunId: %q }`, testRunID), + ); err != nil { + return fmt.Errorf("adding k6 object to new browser context: %w", err) + } + + return nil +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/console_message_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/console_message_mapping.go new file mode 100644 index 00000000000..226baaa68ad --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/console_message_mapping.go @@ -0,0 +1,38 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" +) + +// mapConsoleMessage to the JS module. +func mapConsoleMessage(vu moduleVU, cm *common.ConsoleMessage) mapping { + rt := vu.Runtime() + return mapping{ + "args": func() *sobek.Object { + var ( + margs []mapping + args = cm.Args + ) + for _, arg := range args { + a := mapJSHandle(vu, arg) + margs = append(margs, a) + } + + return rt.ToValue(margs).ToObject(rt) + }, + // page(), text() and type() are defined as + // functions in order to match Playwright's API + "page": func() *sobek.Object { + mp := mapPage(vu, cm.Page) + return rt.ToValue(mp).ToObject(rt) + }, + "text": func() *sobek.Object { + return rt.ToValue(cm.Text).ToObject(rt) + }, + "type": func() *sobek.Object { + return rt.ToValue(cm.Type).ToObject(rt) + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/element_handle_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/element_handle_mapping.go new file mode 100644 index 00000000000..c598ce739d2 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/element_handle_mapping.go @@ -0,0 +1,263 @@ +package browser + +import ( + "fmt" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapElementHandle to the JS module. +func mapElementHandle(vu moduleVU, eh *common.ElementHandle) mapping { //nolint:gocognit,cyclop,funlen + rt := vu.Runtime() + maps := mapping{ + "boundingBox": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.BoundingBox(), nil + }) + }, + "check": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Check(opts) //nolint:wrapcheck + }) + }, + "click": func(opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewElementHandleClickOptions(eh.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing element click options: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + err := eh.Click(popts) + return nil, err //nolint:wrapcheck + }), nil + }, + "contentFrame": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + f, err := eh.ContentFrame() + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapFrame(vu, f), nil + }) + }, + "dblclick": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Dblclick(opts) //nolint:wrapcheck + }) + }, + "dispatchEvent": func(typ string, eventInit sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.DispatchEvent(typ, exportArg(eventInit)) //nolint:wrapcheck + }) + }, + "fill": func(value string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Fill(value, opts) //nolint:wrapcheck + }) + }, + "focus": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Focus() //nolint:wrapcheck + }) + }, + "getAttribute": func(name string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + s, ok, err := eh.GetAttribute(name) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil + }) + }, + "hover": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Hover(opts) //nolint:wrapcheck + }) + }, + "innerHTML": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.InnerHTML() //nolint:wrapcheck + }) + }, + "innerText": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.InnerText() //nolint:wrapcheck + }) + }, + "inputValue": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.InputValue(opts) //nolint:wrapcheck + }) + }, + "isChecked": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.IsChecked() //nolint:wrapcheck + }) + }, + "isDisabled": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.IsDisabled() //nolint:wrapcheck + }) + }, + "isEditable": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.IsEditable() //nolint:wrapcheck + }) + }, + "isEnabled": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.IsEnabled() //nolint:wrapcheck + }) + }, + "isHidden": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.IsHidden() //nolint:wrapcheck + }) + }, + "isVisible": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.IsVisible() //nolint:wrapcheck + }) + }, + "ownerFrame": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + f, err := eh.OwnerFrame() + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapFrame(vu, f), nil + }) + }, + "press": func(key string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Press(key, opts) //nolint:wrapcheck + }) + }, + "screenshot": func(opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewElementHandleScreenshotOptions(eh.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing element handle screenshot options: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + bb, err := eh.Screenshot(popts, vu.filePersister) + if err != nil { + return nil, err //nolint:wrapcheck + } + + ab := rt.NewArrayBuffer(bb) + + return &ab, nil + }), nil + }, + "scrollIntoViewIfNeeded": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.ScrollIntoViewIfNeeded(opts) //nolint:wrapcheck + }) + }, + "selectOption": func(values sobek.Value, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return eh.SelectOption(values, opts) //nolint:wrapcheck + }) + }, + "selectText": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.SelectText(opts) //nolint:wrapcheck + }) + }, + "setInputFiles": func(files sobek.Value, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.SetInputFiles(files, opts) //nolint:wrapcheck + }) + }, + "tap": func(opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewElementHandleTapOptions(eh.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing element tap options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Tap(popts) //nolint:wrapcheck + }), nil + }, + "textContent": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + s, ok, err := eh.TextContent() + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil + }) + }, + "type": func(text string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Type(text, opts) //nolint:wrapcheck + }) + }, + "uncheck": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Uncheck(opts) //nolint:wrapcheck + }) + }, + "waitForElementState": func(state string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.WaitForElementState(state, opts) //nolint:wrapcheck + }) + }, + "waitForSelector": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + eh, err := eh.WaitForSelector(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapElementHandle(vu, eh), nil + }) + }, + } + maps["$"] = func(selector string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + eh, err := eh.Query(selector, common.StrictModeOff) + if err != nil { + return nil, err //nolint:wrapcheck + } + // ElementHandle can be null when the selector does not match any elements. + // We do not want to map nil elementHandles since the expectation is a + // null result in the test script for this case. + if eh == nil { + return nil, nil + } + ehm := mapElementHandle(vu, eh) + + return ehm, nil + }) + } + maps["$$"] = func(selector string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + ehs, err := eh.QueryAll(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + var mehs []mapping + for _, eh := range ehs { + ehm := mapElementHandle(vu, eh) + mehs = append(mehs, ehm) + } + return mehs, nil + }) + } + + jsHandleMap := mapJSHandle(vu, eh) + for k, v := range jsHandleMap { + maps[k] = v + } + + return maps +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/frame_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/frame_mapping.go new file mode 100644 index 00000000000..4d1aaf3a694 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/frame_mapping.go @@ -0,0 +1,322 @@ +package browser + +import ( + "fmt" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapFrame to the JS module. +// +//nolint:funlen +func mapFrame(vu moduleVU, f *common.Frame) mapping { //nolint:gocognit,cyclop + maps := mapping{ + "check": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Check(selector, opts) //nolint:wrapcheck + }) + }, + "childFrames": func() []mapping { + var ( + mcfs []mapping + cfs = f.ChildFrames() + ) + for _, fr := range cfs { + mcfs = append(mcfs, mapFrame(vu, fr)) + } + return mcfs + }, + "click": func(selector string, opts sobek.Value) (*sobek.Promise, error) { + popts, err := parseFrameClickOptions(vu.Context(), opts, f.Timeout()) + if err != nil { + return nil, err + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + err := f.Click(selector, popts) + return nil, err //nolint:wrapcheck + }), nil + }, + "content": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.Content() //nolint:wrapcheck + }) + }, + "dblclick": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Dblclick(selector, opts) //nolint:wrapcheck + }) + }, + "dispatchEvent": func(selector, typ string, eventInit, opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameDispatchEventOptions(f.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing frame dispatch event options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck + }), nil + }, + "evaluate": func(pageFunction sobek.Value, gargs ...sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.Evaluate(pageFunction.String(), exportArgs(gargs)...) //nolint:wrapcheck + }) + }, + "evaluateHandle": func(pageFunction sobek.Value, gargs ...sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + jsh, err := f.EvaluateHandle(pageFunction.String(), exportArgs(gargs)...) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapJSHandle(vu, jsh), nil + }) + }, + "fill": func(selector, value string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Fill(selector, value, opts) //nolint:wrapcheck + }) + }, + "focus": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Focus(selector, opts) //nolint:wrapcheck + }) + }, + "frameElement": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + fe, err := f.FrameElement() + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapElementHandle(vu, fe), nil + }) + }, + "getAttribute": func(selector, name string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + s, ok, err := f.GetAttribute(selector, name, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil + }) + }, + "goto": func(url string, opts sobek.Value) (*sobek.Promise, error) { + gopts := common.NewFrameGotoOptions( + f.Referrer(), + f.NavigationTimeout(), + ) + if err := gopts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing frame navigation options to %q: %w", url, err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + resp, err := f.Goto(url, gopts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + return mapResponse(vu, resp), nil + }), nil + }, + "hover": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Hover(selector, opts) //nolint:wrapcheck + }) + }, + "innerHTML": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.InnerHTML(selector, opts) //nolint:wrapcheck + }) + }, + "innerText": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.InnerText(selector, opts) //nolint:wrapcheck + }) + }, + "inputValue": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.InputValue(selector, opts) //nolint:wrapcheck + }) + }, + "isChecked": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.IsChecked(selector, opts) //nolint:wrapcheck + }) + }, + "isDetached": f.IsDetached, + "isDisabled": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.IsDisabled(selector, opts) //nolint:wrapcheck + }) + }, + "isEditable": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.IsEditable(selector, opts) //nolint:wrapcheck + }) + }, + "isEnabled": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.IsEnabled(selector, opts) //nolint:wrapcheck + }) + }, + "isHidden": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.IsHidden(selector, opts) //nolint:wrapcheck + }) + }, + "isVisible": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.IsVisible(selector, opts) //nolint:wrapcheck + }) + }, + "locator": func(selector string, opts sobek.Value) mapping { + return mapLocator(vu, f.Locator(selector, opts)) + }, + "name": f.Name, + "page": func() mapping { + return mapPage(vu, f.Page()) + }, + "parentFrame": func() mapping { + return mapFrame(vu, f.ParentFrame()) + }, + "press": func(selector, key string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Press(selector, key, opts) //nolint:wrapcheck + }) + }, + "selectOption": func(selector string, values sobek.Value, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.SelectOption(selector, values, opts) //nolint:wrapcheck + }) + }, + "setContent": func(html string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.SetContent(html, opts) //nolint:wrapcheck + }) + }, + "setInputFiles": func(selector string, files sobek.Value, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.SetInputFiles(selector, files, opts) //nolint:wrapcheck + }) + }, + "tap": func(selector string, opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameTapOptions(f.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing frame tap options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Tap(selector, popts) //nolint:wrapcheck + }), nil + }, + "textContent": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + s, ok, err := f.TextContent(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil + }) + }, + "title": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return f.Title(), nil + }) + }, + "type": func(selector, text string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Type(selector, text, opts) //nolint:wrapcheck + }) + }, + "uncheck": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Uncheck(selector, opts) //nolint:wrapcheck + }) + }, + "url": f.URL, + "waitForFunction": func(pageFunc, opts sobek.Value, args ...sobek.Value) (*sobek.Promise, error) { + js, popts, pargs, err := parseWaitForFunctionArgs( + vu.Context(), f.Timeout(), pageFunc, opts, args..., + ) + if err != nil { + return nil, fmt.Errorf("frame waitForFunction: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + return f.WaitForFunction(js, popts, pargs...) //nolint:wrapcheck + }), nil + }, + "waitForLoadState": func(state string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.WaitForLoadState(state, opts) //nolint:wrapcheck + }) + }, + "waitForNavigation": func(opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameWaitForNavigationOptions(f.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing frame wait for navigation options: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + resp, err := f.WaitForNavigation(popts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapResponse(vu, resp), nil + }), nil + }, + "waitForSelector": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + eh, err := f.WaitForSelector(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapElementHandle(vu, eh), nil + }) + }, + "waitForTimeout": func(timeout int64) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + f.WaitForTimeout(timeout) + return nil, nil + }) + }, + } + maps["$"] = func(selector string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + eh, err := f.Query(selector, common.StrictModeOff) + if err != nil { + return nil, err //nolint:wrapcheck + } + // ElementHandle can be null when the selector does not match any elements. + // We do not want to map nil elementHandles since the expectation is a + // null result in the test script for this case. + if eh == nil { + return nil, nil + } + ehm := mapElementHandle(vu, eh) + + return ehm, nil + }) + } + maps["$$"] = func(selector string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + ehs, err := f.QueryAll(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + var mehs []mapping + for _, eh := range ehs { + ehm := mapElementHandle(vu, eh) + mehs = append(mehs, ehm) + } + return mehs, nil + }) + } + + return maps +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/helpers.go b/vendor/github.com/grafana/xk6-browser/browser/helpers.go index 47375281f11..ba93f28f57f 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/helpers.go +++ b/vendor/github.com/grafana/xk6-browser/browser/helpers.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/k6error" "github.com/grafana/xk6-browser/k6ext" @@ -18,15 +18,15 @@ func panicIfFatalError(ctx context.Context, 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) { +func exportArg(gv sobek.Value) any { + if !sobekValueExists(gv) { return nil } return gv.Export() } -// exportArgs returns a slice of exported Goja values. -func exportArgs(gargs []goja.Value) []any { +// exportArgs returns a slice of exported sobek values. +func exportArgs(gargs []sobek.Value) []any { args := make([]any, 0, len(gargs)) for _, garg := range gargs { // leaves a nil garg in the array since users might want to @@ -36,8 +36,8 @@ func exportArgs(gargs []goja.Value) []any { 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) +// sobekValueExists returns true if a given value is not nil and exists +// (defined and not null) in the sobek runtime. +func sobekValueExists(v sobek.Value) bool { + return v != nil && !sobek.IsUndefined(v) && !sobek.IsNull(v) } diff --git a/vendor/github.com/grafana/xk6-browser/browser/js_handle_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/js_handle_mapping.go new file mode 100644 index 00000000000..db300a6d5a6 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/js_handle_mapping.go @@ -0,0 +1,59 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapJSHandle to the JS module. +func mapJSHandle(vu moduleVU, jsh common.JSHandleAPI) mapping { + return mapping{ + "asElement": func() mapping { + return mapElementHandle(vu, jsh.AsElement()) + }, + "dispose": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, jsh.Dispose() //nolint:wrapcheck + }) + }, + "evaluate": func(pageFunc sobek.Value, gargs ...sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + args := make([]any, 0, len(gargs)) + for _, a := range gargs { + args = append(args, exportArg(a)) + } + return jsh.Evaluate(pageFunc.String(), args...) //nolint:wrapcheck + }) + }, + "evaluateHandle": func(pageFunc sobek.Value, gargs ...sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + h, err := jsh.EvaluateHandle(pageFunc.String(), exportArgs(gargs)...) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapJSHandle(vu, h), nil + }) + }, + "getProperties": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + props, err := jsh.GetProperties() + if err != nil { + return nil, err //nolint:wrapcheck + } + + dst := make(map[string]any) + for k, v := range props { + dst[k] = mapJSHandle(vu, v) + } + return dst, nil + }) + }, + "jsonValue": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return jsh.JSONValue() //nolint:wrapcheck + }) + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/keyboard_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/keyboard_mapping.go new file mode 100644 index 00000000000..0fe9cc8ff2f --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/keyboard_mapping.go @@ -0,0 +1,38 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +func mapKeyboard(vu moduleVU, kb *common.Keyboard) mapping { + return mapping{ + "down": func(key string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, kb.Down(key) //nolint:wrapcheck + }) + }, + "up": func(key string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, kb.Up(key) //nolint:wrapcheck + }) + }, + "press": func(key string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, kb.Press(key, opts) //nolint:wrapcheck + }) + }, + "type": func(text string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, kb.Type(text, opts) //nolint:wrapcheck + }) + }, + "insertText": func(text string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, kb.InsertText(text) //nolint:wrapcheck + }) + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/locator_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/locator_mapping.go new file mode 100644 index 00000000000..1df2f73ecdf --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/locator_mapping.go @@ -0,0 +1,172 @@ +package browser + +import ( + "fmt" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapLocator API to the JS module. +func mapLocator(vu moduleVU, lo *common.Locator) mapping { //nolint:funlen + return mapping{ + "clear": func(opts sobek.Value) (*sobek.Promise, error) { + copts := common.NewFrameFillOptions(lo.Timeout()) + if err := copts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing clear options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Clear(copts) //nolint:wrapcheck + }), nil + }, + "click": func(opts sobek.Value) (*sobek.Promise, error) { + popts, err := parseFrameClickOptions(vu.Context(), opts, lo.Timeout()) + if err != nil { + return nil, err + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Click(popts) //nolint:wrapcheck + }), nil + }, + "dblclick": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Dblclick(opts) //nolint:wrapcheck + }) + }, + "check": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Check(opts) //nolint:wrapcheck + }) + }, + "uncheck": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Uncheck(opts) //nolint:wrapcheck + }) + }, + "isChecked": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.IsChecked(opts) //nolint:wrapcheck + }) + }, + "isEditable": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.IsEditable(opts) //nolint:wrapcheck + }) + }, + "isEnabled": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.IsEnabled(opts) //nolint:wrapcheck + }) + }, + "isDisabled": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.IsDisabled(opts) //nolint:wrapcheck + }) + }, + "isVisible": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.IsVisible() //nolint:wrapcheck + }) + }, + "isHidden": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.IsHidden() //nolint:wrapcheck + }) + }, + "fill": func(value string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Fill(value, opts) //nolint:wrapcheck + }) + }, + "focus": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Focus(opts) //nolint:wrapcheck + }) + }, + "getAttribute": func(name string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + s, ok, err := lo.GetAttribute(name, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil + }) + }, + "innerHTML": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.InnerHTML(opts) //nolint:wrapcheck + }) + }, + "innerText": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.InnerText(opts) //nolint:wrapcheck + }) + }, + "textContent": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + s, ok, err := lo.TextContent(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil + }) + }, + "inputValue": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.InputValue(opts) //nolint:wrapcheck + }) + }, + "selectOption": func(values sobek.Value, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return lo.SelectOption(values, opts) //nolint:wrapcheck + }) + }, + "press": func(key string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Press(key, opts) //nolint:wrapcheck + }) + }, + "type": func(text string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Type(text, opts) //nolint:wrapcheck + }) + }, + "hover": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Hover(opts) //nolint:wrapcheck + }) + }, + "tap": func(opts sobek.Value) (*sobek.Promise, error) { + copts := common.NewFrameTapOptions(lo.DefaultTimeout()) + if err := copts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing locator tap options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Tap(copts) //nolint:wrapcheck + }), nil + }, + "dispatchEvent": func(typ string, eventInit, opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameDispatchEventOptions(lo.DefaultTimeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing locator dispatch event options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.DispatchEvent(typ, exportArg(eventInit), popts) //nolint:wrapcheck + }), nil + }, + "waitFor": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.WaitFor(opts) //nolint:wrapcheck + }) + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/mapping.go b/vendor/github.com/grafana/xk6-browser/browser/mapping.go index 9697af4ceae..80cc7cbfaa2 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/mapping.go +++ b/vendor/github.com/grafana/xk6-browser/browser/mapping.go @@ -3,28 +3,25 @@ package browser import ( "context" "fmt" - "reflect" "time" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/common" - "github.com/grafana/xk6-browser/k6error" - "github.com/grafana/xk6-browser/k6ext" k6common "go.k6.io/k6/js/common" ) -// mapping is a type for mapping our module API to Goja. +// mapping is a type for mapping our module API to sobek. // It acts like a bridge and allows adding wildcard methods // and customization over our API. type mapping = map[string]any -// mapBrowserToGoja maps the browser API to the JS module. +// mapBrowserToSobek maps the browser API to the JS module. // The motivation of this mapping was to support $ and $$ wildcard // methods. // See issue #661 for more details. -func mapBrowserToGoja(vu moduleVU) *goja.Object { +func mapBrowserToSobek(vu moduleVU) *sobek.Object { var ( rt = vu.Runtime() obj = rt.NewObject() @@ -39,71 +36,8 @@ func mapBrowserToGoja(vu moduleVU) *goja.Object { return obj } -// mapLocator API to the JS module. -func mapLocator(vu moduleVU, lo *common.Locator) mapping { - return mapping{ - "clear": func(opts goja.Value) error { - ctx := vu.Context() - - copts := common.NewFrameFillOptions(lo.Timeout()) - if err := copts.Parse(ctx, opts); err != nil { - return fmt.Errorf("parsing clear options: %w", err) - } - - return lo.Clear(copts) //nolint:wrapcheck - }, - "click": func(opts goja.Value) (*goja.Promise, error) { - popts, err := parseFrameClickOptions(vu.Context(), opts, lo.Timeout()) - if err != nil { - return nil, err - } - - return k6ext.Promise(vu.Context(), func() (any, error) { - return nil, lo.Click(popts) //nolint:wrapcheck - }), nil - }, - "dblclick": lo.Dblclick, - "check": lo.Check, - "uncheck": lo.Uncheck, - "isChecked": lo.IsChecked, - "isEditable": lo.IsEditable, - "isEnabled": lo.IsEnabled, - "isDisabled": lo.IsDisabled, - "isVisible": lo.IsVisible, - "isHidden": lo.IsHidden, - "fill": lo.Fill, - "focus": lo.Focus, - "getAttribute": lo.GetAttribute, - "innerHTML": lo.InnerHTML, - "innerText": lo.InnerText, - "textContent": lo.TextContent, - "inputValue": lo.InputValue, - "selectOption": lo.SelectOption, - "press": lo.Press, - "type": lo.Type, - "hover": lo.Hover, - "tap": func(opts goja.Value) (*goja.Promise, error) { - copts := common.NewFrameTapOptions(lo.DefaultTimeout()) - if err := copts.Parse(vu.Context(), opts); err != nil { - return nil, fmt.Errorf("parsing locator tap options: %w", err) - } - return k6ext.Promise(vu.Context(), func() (any, error) { - return nil, lo.Tap(copts) //nolint:wrapcheck - }), nil - }, - "dispatchEvent": func(typ string, eventInit, opts goja.Value) error { - popts := common.NewFrameDispatchEventOptions(lo.DefaultTimeout()) - if err := popts.Parse(vu.Context(), opts); err != nil { - return fmt.Errorf("parsing locator dispatch event options: %w", err) - } - return lo.DispatchEvent(typ, exportArg(eventInit), popts) //nolint:wrapcheck - }, - "waitFor": lo.WaitFor, - } -} - func parseFrameClickOptions( - ctx context.Context, opts goja.Value, defaultTimeout time.Duration, + ctx context.Context, opts sobek.Value, defaultTimeout time.Duration, ) (*common.FrameClickOptions, error) { copts := common.NewFrameClickOptions(defaultTimeout) if err := copts.Parse(ctx, opts); err != nil { @@ -111,937 +45,3 @@ func parseFrameClickOptions( } return copts, nil } - -// mapRequest to the JS module. -func mapRequest(vu moduleVU, r *common.Request) mapping { - rt := vu.Runtime() - maps := mapping{ - "allHeaders": r.AllHeaders, - "frame": func() *goja.Object { - mf := mapFrame(vu, r.Frame()) - return rt.ToValue(mf).ToObject(rt) - }, - "headerValue": r.HeaderValue, - "headers": r.Headers, - "headersArray": r.HeadersArray, - "isNavigationRequest": r.IsNavigationRequest, - "method": r.Method, - "postData": r.PostData, - "postDataBuffer": r.PostDataBuffer, - "resourceType": r.ResourceType, - "response": func() *goja.Object { - mr := mapResponse(vu, r.Response()) - return rt.ToValue(mr).ToObject(rt) - }, - "size": r.Size, - "timing": r.Timing, - "url": r.URL, - } - - return maps -} - -// mapResponse to the JS module. -func mapResponse(vu moduleVU, r *common.Response) mapping { - if r == nil { - return nil - } - rt := vu.Runtime() - maps := mapping{ - "allHeaders": r.AllHeaders, - "body": r.Body, - "frame": func() *goja.Object { - mf := mapFrame(vu, r.Frame()) - return rt.ToValue(mf).ToObject(rt) - }, - "headerValue": r.HeaderValue, - "headerValues": r.HeaderValues, - "headers": r.Headers, - "headersArray": r.HeadersArray, - "json": r.JSON, - "ok": r.Ok, - "request": func() *goja.Object { - mr := mapRequest(vu, r.Request()) - return rt.ToValue(mr).ToObject(rt) - }, - "securityDetails": r.SecurityDetails, - "serverAddr": r.ServerAddr, - "size": r.Size, - "status": r.Status, - "statusText": r.StatusText, - "url": r.URL, - } - - return maps -} - -// mapJSHandle to the JS module. -func mapJSHandle(vu moduleVU, jsh common.JSHandleAPI) mapping { - rt := vu.Runtime() - return mapping{ - "asElement": func() *goja.Object { - m := mapElementHandle(vu, jsh.AsElement()) - return rt.ToValue(m).ToObject(rt) - }, - "dispose": jsh.Dispose, - "evaluate": func(pageFunc goja.Value, gargs ...goja.Value) any { - args := make([]any, 0, len(gargs)) - for _, a := range gargs { - args = append(args, exportArg(a)) - } - return jsh.Evaluate(pageFunc.String(), args...) - }, - "evaluateHandle": func(pageFunc goja.Value, gargs ...goja.Value) (mapping, error) { - h, err := jsh.EvaluateHandle(pageFunc.String(), exportArgs(gargs)...) - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapJSHandle(vu, h), nil - }, - "getProperties": func() (mapping, error) { - props, err := jsh.GetProperties() - if err != nil { - return nil, err //nolint:wrapcheck - } - - dst := make(map[string]any) - for k, v := range props { - dst[k] = mapJSHandle(vu, v) - } - return dst, nil - }, - "jsonValue": jsh.JSONValue, - } -} - -// mapElementHandle to the JS module. -// -//nolint:funlen -func mapElementHandle(vu moduleVU, eh *common.ElementHandle) mapping { - rt := vu.Runtime() - maps := mapping{ - "boundingBox": eh.BoundingBox, - "check": eh.Check, - "click": func(opts goja.Value) (*goja.Promise, error) { - ctx := vu.Context() - - popts := common.NewElementHandleClickOptions(eh.Timeout()) - if err := popts.Parse(ctx, opts); err != nil { - return nil, fmt.Errorf("parsing element click options: %w", err) - } - - return k6ext.Promise(vu.Context(), func() (any, error) { - err := eh.Click(popts) - return nil, err //nolint:wrapcheck - }), nil - }, - "contentFrame": func() (mapping, error) { - f, err := eh.ContentFrame() - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapFrame(vu, f), nil - }, - "dblclick": eh.Dblclick, - "dispatchEvent": func(typ string, eventInit goja.Value) error { - return eh.DispatchEvent(typ, exportArg(eventInit)) //nolint:wrapcheck - }, - "fill": eh.Fill, - "focus": eh.Focus, - "getAttribute": eh.GetAttribute, - "hover": eh.Hover, - "innerHTML": eh.InnerHTML, - "innerText": eh.InnerText, - "inputValue": eh.InputValue, - "isChecked": eh.IsChecked, - "isDisabled": eh.IsDisabled, - "isEditable": eh.IsEditable, - "isEnabled": eh.IsEnabled, - "isHidden": eh.IsHidden, - "isVisible": eh.IsVisible, - "ownerFrame": func() (mapping, error) { - f, err := eh.OwnerFrame() - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapFrame(vu, f), nil - }, - "press": eh.Press, - "screenshot": func(opts goja.Value) (*goja.ArrayBuffer, error) { - ctx := vu.Context() - - popts := common.NewElementHandleScreenshotOptions(eh.Timeout()) - if err := popts.Parse(ctx, opts); err != nil { - return nil, fmt.Errorf("parsing frame screenshot options: %w", err) - } - - bb, err := eh.Screenshot(popts, vu.filePersister) - if err != nil { - return nil, err //nolint:wrapcheck - } - - ab := rt.NewArrayBuffer(bb) - - return &ab, nil - }, - "scrollIntoViewIfNeeded": eh.ScrollIntoViewIfNeeded, - "selectOption": eh.SelectOption, - "selectText": eh.SelectText, - "setInputFiles": eh.SetInputFiles, - "tap": func(opts goja.Value) (*goja.Promise, error) { - popts := common.NewElementHandleTapOptions(eh.Timeout()) - if err := popts.Parse(vu.Context(), opts); err != nil { - return nil, fmt.Errorf("parsing element tap options: %w", err) - } - return k6ext.Promise(vu.Context(), func() (any, error) { - return nil, eh.Tap(popts) //nolint:wrapcheck - }), nil - }, - "textContent": eh.TextContent, - "type": eh.Type, - "uncheck": eh.Uncheck, - "waitForElementState": eh.WaitForElementState, - "waitForSelector": func(selector string, opts goja.Value) (mapping, error) { - eh, err := eh.WaitForSelector(selector, opts) - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapElementHandle(vu, eh), nil - }, - } - maps["$"] = func(selector string) (mapping, error) { - eh, err := eh.Query(selector, common.StrictModeOff) - if err != nil { - return nil, err //nolint:wrapcheck - } - // ElementHandle can be null when the selector does not match any elements. - // We do not want to map nil elementHandles since the expectation is a - // null result in the test script for this case. - if eh == nil { - return nil, nil //nolint:nilnil - } - ehm := mapElementHandle(vu, eh) - return ehm, nil - } - maps["$$"] = func(selector string) ([]mapping, error) { - ehs, err := eh.QueryAll(selector) - if err != nil { - return nil, err //nolint:wrapcheck - } - var mehs []mapping - for _, eh := range ehs { - ehm := mapElementHandle(vu, eh) - mehs = append(mehs, ehm) - } - return mehs, nil - } - - jsHandleMap := mapJSHandle(vu, eh) - for k, v := range jsHandleMap { - maps[k] = v - } - - return maps -} - -// mapFrame to the JS module. -// -//nolint:funlen -func mapFrame(vu moduleVU, f *common.Frame) mapping { - rt := vu.Runtime() - maps := mapping{ - "check": f.Check, - "childFrames": func() *goja.Object { - var ( - mcfs []mapping - cfs = f.ChildFrames() - ) - for _, fr := range cfs { - mcfs = append(mcfs, mapFrame(vu, fr)) - } - return rt.ToValue(mcfs).ToObject(rt) - }, - "click": func(selector string, opts goja.Value) (*goja.Promise, error) { - popts, err := parseFrameClickOptions(vu.Context(), opts, f.Timeout()) - if err != nil { - return nil, err - } - - return k6ext.Promise(vu.Context(), func() (any, error) { - err := f.Click(selector, popts) - return nil, err //nolint:wrapcheck - }), nil - }, - "content": f.Content, - "dblclick": f.Dblclick, - "dispatchEvent": func(selector, typ string, eventInit, opts goja.Value) error { - popts := common.NewFrameDispatchEventOptions(f.Timeout()) - if err := popts.Parse(vu.Context(), opts); err != nil { - return fmt.Errorf("parsing frame dispatch event options: %w", err) - } - return f.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck - }, - "evaluate": func(pageFunction goja.Value, gargs ...goja.Value) any { - return f.Evaluate(pageFunction.String(), exportArgs(gargs)...) - }, - "evaluateHandle": func(pageFunction goja.Value, gargs ...goja.Value) (mapping, error) { - jsh, err := f.EvaluateHandle(pageFunction.String(), exportArgs(gargs)...) - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapJSHandle(vu, jsh), nil - }, - "fill": f.Fill, - "focus": f.Focus, - "frameElement": func() (mapping, error) { - fe, err := f.FrameElement() - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapElementHandle(vu, fe), nil - }, - "getAttribute": f.GetAttribute, - "goto": func(url string, opts goja.Value) (*goja.Promise, error) { - gopts := common.NewFrameGotoOptions( - f.Referrer(), - f.NavigationTimeout(), - ) - if err := gopts.Parse(vu.Context(), opts); err != nil { - return nil, fmt.Errorf("parsing frame navigation options to %q: %w", url, err) - } - return k6ext.Promise(vu.Context(), func() (any, error) { - resp, err := f.Goto(url, gopts) - if err != nil { - return nil, err //nolint:wrapcheck - } - - return mapResponse(vu, resp), nil - }), nil - }, - "hover": f.Hover, - "innerHTML": f.InnerHTML, - "innerText": f.InnerText, - "inputValue": f.InputValue, - "isChecked": f.IsChecked, - "isDetached": f.IsDetached, - "isDisabled": f.IsDisabled, - "isEditable": f.IsEditable, - "isEnabled": f.IsEnabled, - "isHidden": f.IsHidden, - "isVisible": f.IsVisible, - "locator": func(selector string, opts goja.Value) *goja.Object { - ml := mapLocator(vu, f.Locator(selector, opts)) - return rt.ToValue(ml).ToObject(rt) - }, - "name": f.Name, - "page": func() *goja.Object { - mp := mapPage(vu, f.Page()) - return rt.ToValue(mp).ToObject(rt) - }, - "parentFrame": func() *goja.Object { - mf := mapFrame(vu, f.ParentFrame()) - return rt.ToValue(mf).ToObject(rt) - }, - "press": f.Press, - "selectOption": f.SelectOption, - "setContent": f.SetContent, - "setInputFiles": f.SetInputFiles, - "tap": func(selector string, opts goja.Value) (*goja.Promise, error) { - popts := common.NewFrameTapOptions(f.Timeout()) - if err := popts.Parse(vu.Context(), opts); err != nil { - return nil, fmt.Errorf("parsing frame tap options: %w", err) - } - return k6ext.Promise(vu.Context(), func() (any, error) { - return nil, f.Tap(selector, popts) //nolint:wrapcheck - }), nil - }, - "textContent": f.TextContent, - "title": f.Title, - "type": f.Type, - "uncheck": f.Uncheck, - "url": f.URL, - "waitForFunction": func(pageFunc, opts goja.Value, args ...goja.Value) (*goja.Promise, error) { - js, popts, pargs, err := parseWaitForFunctionArgs( - vu.Context(), f.Timeout(), pageFunc, opts, args..., - ) - if err != nil { - return nil, fmt.Errorf("frame waitForFunction: %w", err) - } - - return k6ext.Promise(vu.Context(), func() (result any, reason error) { - return f.WaitForFunction(js, popts, pargs...) //nolint:wrapcheck - }), nil - }, - "waitForLoadState": f.WaitForLoadState, - "waitForNavigation": func(opts goja.Value) (*goja.Promise, error) { - popts := common.NewFrameWaitForNavigationOptions(f.Timeout()) - if err := popts.Parse(vu.Context(), opts); err != nil { - return nil, fmt.Errorf("parsing frame wait for navigation options: %w", err) - } - - return k6ext.Promise(vu.Context(), func() (result any, reason error) { - resp, err := f.WaitForNavigation(popts) - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapResponse(vu, resp), nil - }), nil - }, - "waitForSelector": func(selector string, opts goja.Value) (mapping, error) { - eh, err := f.WaitForSelector(selector, opts) - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapElementHandle(vu, eh), nil - }, - "waitForTimeout": f.WaitForTimeout, - } - maps["$"] = func(selector string) (mapping, error) { - eh, err := f.Query(selector, common.StrictModeOff) - if err != nil { - return nil, err //nolint:wrapcheck - } - // ElementHandle can be null when the selector does not match any elements. - // We do not want to map nil elementHandles since the expectation is a - // null result in the test script for this case. - if eh == nil { - return nil, nil //nolint:nilnil - } - ehm := mapElementHandle(vu, eh) - return ehm, nil - } - maps["$$"] = func(selector string) ([]mapping, error) { - ehs, err := f.QueryAll(selector) - if err != nil { - return nil, err //nolint:wrapcheck - } - var mehs []mapping - for _, eh := range ehs { - ehm := mapElementHandle(vu, eh) - mehs = append(mehs, ehm) - } - return mehs, nil - } - - return maps -} - -func parseWaitForFunctionArgs( - 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) - if err != nil { - return "", nil, nil, fmt.Errorf("parsing waitForFunction options: %w", err) - } - - js := pageFunc.ToString().String() - _, isCallable := goja.AssertFunction(pageFunc) - if !isCallable { - js = fmt.Sprintf("() => (%s)", js) - } - - return js, popts, exportArgs(gargs), nil -} - -// mapPage to the JS module. -// -//nolint:funlen -func mapPage(vu moduleVU, p *common.Page) mapping { - rt := vu.Runtime() - maps := mapping{ - "bringToFront": p.BringToFront, - "check": p.Check, - "click": func(selector string, opts goja.Value) (*goja.Promise, error) { - popts, err := parseFrameClickOptions(vu.Context(), opts, p.Timeout()) - if err != nil { - return nil, err - } - - return k6ext.Promise(vu.Context(), func() (any, error) { - err := p.Click(selector, popts) - return nil, err //nolint:wrapcheck - }), nil - }, - "close": func(opts goja.Value) error { - vu.taskQueueRegistry.close(p.TargetID()) - - return p.Close(opts) //nolint:wrapcheck - }, - "content": p.Content, - "context": p.Context, - "dblclick": p.Dblclick, - "dispatchEvent": func(selector, typ string, eventInit, opts goja.Value) error { - popts := common.NewFrameDispatchEventOptions(p.Timeout()) - if err := popts.Parse(vu.Context(), opts); err != nil { - return fmt.Errorf("parsing page dispatch event options: %w", err) - } - return p.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck - }, - "emulateMedia": p.EmulateMedia, - "emulateVisionDeficiency": p.EmulateVisionDeficiency, - "evaluate": func(pageFunction goja.Value, gargs ...goja.Value) any { - return p.Evaluate(pageFunction.String(), exportArgs(gargs)...) - }, - "evaluateHandle": func(pageFunc goja.Value, gargs ...goja.Value) (mapping, error) { - jsh, err := p.EvaluateHandle(pageFunc.String(), exportArgs(gargs)...) - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapJSHandle(vu, jsh), nil - }, - "fill": p.Fill, - "focus": p.Focus, - "frames": func() *goja.Object { - var ( - mfrs []mapping - frs = p.Frames() - ) - for _, fr := range frs { - mfrs = append(mfrs, mapFrame(vu, fr)) - } - return rt.ToValue(mfrs).ToObject(rt) - }, - "getAttribute": p.GetAttribute, - "goto": func(url string, opts goja.Value) (*goja.Promise, error) { - gopts := common.NewFrameGotoOptions( - p.Referrer(), - p.NavigationTimeout(), - ) - if err := gopts.Parse(vu.Context(), opts); err != nil { - return nil, fmt.Errorf("parsing page navigation options to %q: %w", url, err) - } - return k6ext.Promise(vu.Context(), func() (any, error) { - resp, err := p.Goto(url, gopts) - if err != nil { - return nil, err //nolint:wrapcheck - } - - return mapResponse(vu, resp), nil - }), nil - }, - "hover": p.Hover, - "innerHTML": p.InnerHTML, - "innerText": p.InnerText, - "inputValue": p.InputValue, - "isChecked": p.IsChecked, - "isClosed": p.IsClosed, - "isDisabled": p.IsDisabled, - "isEditable": p.IsEditable, - "isEnabled": p.IsEnabled, - "isHidden": p.IsHidden, - "isVisible": p.IsVisible, - "keyboard": rt.ToValue(p.GetKeyboard()).ToObject(rt), - "locator": func(selector string, opts goja.Value) *goja.Object { - ml := mapLocator(vu, p.Locator(selector, opts)) - return rt.ToValue(ml).ToObject(rt) - }, - "mainFrame": func() *goja.Object { - mf := mapFrame(vu, p.MainFrame()) - return rt.ToValue(mf).ToObject(rt) - }, - "mouse": rt.ToValue(p.GetMouse()).ToObject(rt), - "on": func(event string, handler goja.Callable) error { - tq := vu.taskQueueRegistry.get(p.TargetID()) - - mapMsgAndHandleEvent := func(m *common.ConsoleMessage) error { - mapping := mapConsoleMessage(vu, m) - _, err := handler(goja.Undefined(), vu.Runtime().ToValue(mapping)) - return err - } - runInTaskQueue := func(m *common.ConsoleMessage) { - tq.Queue(func() error { - if err := mapMsgAndHandleEvent(m); err != nil { - return fmt.Errorf("executing page.on handler: %w", err) - } - return nil - }) - } - - return p.On(event, runInTaskQueue) //nolint:wrapcheck - }, - "opener": p.Opener, - "press": p.Press, - "reload": func(opts goja.Value) (*goja.Object, error) { - resp, err := p.Reload(opts) - if err != nil { - return nil, err //nolint:wrapcheck - } - - r := mapResponse(vu, resp) - - return rt.ToValue(r).ToObject(rt), nil - }, - "screenshot": func(opts goja.Value) (*goja.ArrayBuffer, error) { - ctx := vu.Context() - - popts := common.NewPageScreenshotOptions() - if err := popts.Parse(ctx, opts); err != nil { - return nil, fmt.Errorf("parsing page screenshot options: %w", err) - } - - bb, err := p.Screenshot(popts, vu.filePersister) - if err != nil { - return nil, err //nolint:wrapcheck - } - - ab := rt.NewArrayBuffer(bb) - - return &ab, nil - }, - "selectOption": p.SelectOption, - "setContent": p.SetContent, - "setDefaultNavigationTimeout": p.SetDefaultNavigationTimeout, - "setDefaultTimeout": p.SetDefaultTimeout, - "setExtraHTTPHeaders": p.SetExtraHTTPHeaders, - "setInputFiles": p.SetInputFiles, - "setViewportSize": p.SetViewportSize, - "tap": func(selector string, opts goja.Value) (*goja.Promise, error) { - popts := common.NewFrameTapOptions(p.Timeout()) - if err := popts.Parse(vu.Context(), opts); err != nil { - return nil, fmt.Errorf("parsing page tap options: %w", err) - } - return k6ext.Promise(vu.Context(), func() (any, error) { - return nil, p.Tap(selector, popts) //nolint:wrapcheck - }), nil - }, - "textContent": p.TextContent, - "throttleCPU": p.ThrottleCPU, - "throttleNetwork": p.ThrottleNetwork, - "title": p.Title, - "touchscreen": mapTouchscreen(vu, p.GetTouchscreen()), - "type": p.Type, - "uncheck": p.Uncheck, - "url": p.URL, - "viewportSize": p.ViewportSize, - "waitForFunction": func(pageFunc, opts goja.Value, args ...goja.Value) (*goja.Promise, error) { - js, popts, pargs, err := parseWaitForFunctionArgs( - vu.Context(), p.Timeout(), pageFunc, opts, args..., - ) - if err != nil { - return nil, fmt.Errorf("page waitForFunction: %w", err) - } - - return k6ext.Promise(vu.Context(), func() (result any, reason error) { - return p.WaitForFunction(js, popts, pargs...) //nolint:wrapcheck - }), nil - }, - "waitForLoadState": p.WaitForLoadState, - "waitForNavigation": func(opts goja.Value) (*goja.Promise, error) { - popts := common.NewFrameWaitForNavigationOptions(p.Timeout()) - if err := popts.Parse(vu.Context(), opts); err != nil { - return nil, fmt.Errorf("parsing page wait for navigation options: %w", err) - } - - return k6ext.Promise(vu.Context(), func() (result any, reason error) { - resp, err := p.WaitForNavigation(popts) - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapResponse(vu, resp), nil - }), nil - }, - "waitForSelector": func(selector string, opts goja.Value) (mapping, error) { - eh, err := p.WaitForSelector(selector, opts) - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapElementHandle(vu, eh), nil - }, - "waitForTimeout": p.WaitForTimeout, - "workers": func() *goja.Object { - var mws []mapping - for _, w := range p.Workers() { - mw := mapWorker(vu, w) - mws = append(mws, mw) - } - return rt.ToValue(mws).ToObject(rt) - }, - } - maps["$"] = func(selector string) (mapping, error) { - eh, err := p.Query(selector) - if err != nil { - return nil, err //nolint:wrapcheck - } - // ElementHandle can be null when the selector does not match any elements. - // We do not want to map nil elementHandles since the expectation is a - // null result in the test script for this case. - if eh == nil { - return nil, nil //nolint:nilnil - } - ehm := mapElementHandle(vu, eh) - return ehm, nil - } - maps["$$"] = func(selector string) ([]mapping, error) { - ehs, err := p.QueryAll(selector) - if err != nil { - return nil, err //nolint:wrapcheck - } - var mehs []mapping - for _, eh := range ehs { - ehm := mapElementHandle(vu, eh) - mehs = append(mehs, ehm) - } - return mehs, nil - } - - return maps -} - -// mapTouchscreen to the JS module. -func mapTouchscreen(vu moduleVU, ts *common.Touchscreen) mapping { - return mapping{ - "tap": func(x float64, y float64) *goja.Promise { - return k6ext.Promise(vu.Context(), func() (result any, reason error) { - return nil, ts.Tap(x, y) //nolint:wrapcheck - }) - }, - } -} - -// mapWorker to the JS module. -func mapWorker(vu moduleVU, w *common.Worker) mapping { - return mapping{ - "url": w.URL(), - } -} - -// mapBrowserContext to the JS module. -func mapBrowserContext(vu moduleVU, bc *common.BrowserContext) mapping { //nolint:funlen - rt := vu.Runtime() - return mapping{ - "addCookies": bc.AddCookies, - "addInitScript": func(script goja.Value) error { - if !gojaValueExists(script) { - return nil - } - - source := "" - switch script.ExportType() { - case reflect.TypeOf(string("")): - source = script.String() - case reflect.TypeOf(goja.Object{}): - opts := script.ToObject(rt) - for _, k := range opts.Keys() { - if k == "content" { - source = opts.Get(k).String() - } - } - default: - _, isCallable := goja.AssertFunction(script) - if !isCallable { - source = fmt.Sprintf("(%s);", script.ToString().String()) - } else { - source = fmt.Sprintf("(%s)(...args);", script.ToString().String()) - } - } - - return bc.AddInitScript(source) //nolint:wrapcheck - }, - "browser": bc.Browser, - "clearCookies": bc.ClearCookies, - "clearPermissions": bc.ClearPermissions, - "close": bc.Close, - "cookies": bc.Cookies, - "grantPermissions": func(permissions []string, opts goja.Value) error { - pOpts := common.NewGrantPermissionsOptions() - pOpts.Parse(vu.Context(), opts) - - return bc.GrantPermissions(permissions, pOpts) //nolint:wrapcheck - }, - "setDefaultNavigationTimeout": bc.SetDefaultNavigationTimeout, - "setDefaultTimeout": bc.SetDefaultTimeout, - "setGeolocation": bc.SetGeolocation, - "setHTTPCredentials": bc.SetHTTPCredentials, //nolint:staticcheck - "setOffline": bc.SetOffline, - "waitForEvent": func(event string, optsOrPredicate goja.Value) (*goja.Promise, error) { - ctx := vu.Context() - popts := common.NewWaitForEventOptions( - bc.Timeout(), - ) - if err := popts.Parse(ctx, optsOrPredicate); err != nil { - return nil, fmt.Errorf("parsing waitForEvent options: %w", err) - } - - return k6ext.Promise(ctx, func() (result any, reason error) { - var runInTaskQueue func(p *common.Page) (bool, error) - if popts.PredicateFn != nil { - runInTaskQueue = func(p *common.Page) (bool, error) { - tq := vu.taskQueueRegistry.get(p.TargetID()) - - var rtn bool - var err error - // The function on the taskqueue runs in its own goroutine - // so we need to use a channel to wait for it to complete - // before returning the result to the caller. - c := make(chan bool) - tq.Queue(func() error { - var resp goja.Value - resp, err = popts.PredicateFn(vu.Runtime().ToValue(p)) - rtn = resp.ToBoolean() - close(c) - return nil - }) - <-c - - return rtn, err //nolint:wrapcheck - } - } - - resp, err := bc.WaitForEvent(event, runInTaskQueue, popts.Timeout) - panicIfFatalError(ctx, err) - if err != nil { - return nil, err //nolint:wrapcheck - } - p, ok := resp.(*common.Page) - if !ok { - panicIfFatalError(ctx, fmt.Errorf("response object is not a page: %w", k6error.ErrFatal)) - } - - return mapPage(vu, p), nil - }), nil - }, - "pages": func() *goja.Object { - var ( - mpages []mapping - pages = bc.Pages() - ) - for _, page := range pages { - if page == nil { - continue - } - m := mapPage(vu, page) - mpages = append(mpages, m) - } - - return rt.ToValue(mpages).ToObject(rt) - }, - "newPage": func() (mapping, error) { - page, err := bc.NewPage() - if err != nil { - return nil, err //nolint:wrapcheck - } - return mapPage(vu, page), nil - }, - } -} - -// mapConsoleMessage to the JS module. -func mapConsoleMessage(vu moduleVU, cm *common.ConsoleMessage) mapping { - rt := vu.Runtime() - return mapping{ - "args": func() *goja.Object { - var ( - margs []mapping - args = cm.Args - ) - for _, arg := range args { - a := mapJSHandle(vu, arg) - margs = append(margs, a) - } - - return rt.ToValue(margs).ToObject(rt) - }, - // page(), text() and type() are defined as - // functions in order to match Playwright's API - "page": func() *goja.Object { - mp := mapPage(vu, cm.Page) - return rt.ToValue(mp).ToObject(rt) - }, - "text": func() *goja.Object { - return rt.ToValue(cm.Text).ToObject(rt) - }, - "type": func() *goja.Object { - return rt.ToValue(cm.Type).ToObject(rt) - }, - } -} - -// mapBrowser to the JS module. -func mapBrowser(vu moduleVU) mapping { //nolint:funlen - rt := vu.Runtime() - return mapping{ - "context": func() (*common.BrowserContext, error) { - b, err := vu.browser() - if err != nil { - return nil, err - } - return b.Context(), nil - }, - "closeContext": func() error { - b, err := vu.browser() - if err != nil { - return err - } - return b.CloseContext() //nolint:wrapcheck - }, - "isConnected": func() (bool, error) { - b, err := vu.browser() - if err != nil { - return false, err - } - return b.IsConnected(), nil - }, - "newContext": func(opts goja.Value) (*goja.Object, error) { - b, err := vu.browser() - if err != nil { - return nil, err - } - bctx, err := b.NewContext(opts) - if err != nil { - return nil, err //nolint:wrapcheck - } - - if err := initBrowserContext(bctx, vu.testRunID); err != nil { - return nil, err - } - - m := mapBrowserContext(vu, bctx) - return rt.ToValue(m).ToObject(rt), nil - }, - "userAgent": func() (string, error) { - b, err := vu.browser() - if err != nil { - return "", err - } - return b.UserAgent(), nil - }, - "version": func() (string, error) { - b, err := vu.browser() - if err != nil { - return "", err - } - return b.Version(), nil - }, - "newPage": func(opts goja.Value) (mapping, error) { - b, err := vu.browser() - if err != nil { - return nil, err - } - page, err := b.NewPage(opts) - if err != nil { - return nil, err //nolint:wrapcheck - } - - if err := initBrowserContext(b.Context(), vu.testRunID); err != nil { - return nil, err - } - - return mapPage(vu, page), nil - }, - } -} - -func initBrowserContext(bctx *common.BrowserContext, testRunID string) error { - // Setting a k6 object which will contain k6 specific metadata - // on the current test run. This allows external applications - // (such as Grafana Faro) to identify that the session is a k6 - // automated one and not one driven by a real person. - if err := bctx.AddInitScript( - fmt.Sprintf(`window.k6 = { testRunId: %q }`, testRunID), - ); err != nil { - return fmt.Errorf("adding k6 object to new browser context: %w", err) - } - - return nil -} diff --git a/vendor/github.com/grafana/xk6-browser/browser/module.go b/vendor/github.com/grafana/xk6-browser/browser/module.go index 821fa06ac6f..57bc62c349c 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/module.go +++ b/vendor/github.com/grafana/xk6-browser/browser/module.go @@ -1,6 +1,6 @@ // Package browser is the browser module's entry point, and // initializer of various global types, and a translation layer -// between Goja and the internal business logic. +// between sobek and the internal business logic. // // It initializes and drives the downstream components by passing // the necessary concrete dependencies. @@ -14,7 +14,7 @@ import ( _ "net/http/pprof" //nolint:gosec "sync" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/common" "github.com/grafana/xk6-browser/env" @@ -39,11 +39,12 @@ type ( tracesMetadata map[string]string filePersister filePersister testRunID string + isSync bool // remove later } // JSModule exposes the properties available to the JS script. JSModule struct { - Browser *goja.Object + Browser *sobek.Object Devices map[string]common.Device NetworkProfiles map[string]common.NetworkProfile `js:"networkProfiles"` } @@ -67,6 +68,17 @@ func New() *RootModule { } } +// NewSync returns a pointer to a new RootModule instance that maps the +// browser's business logic to the synchronous version of the module's +// JS API. +func NewSync() *RootModule { + return &RootModule{ + PidRegistry: &pidRegistry{}, + initOnce: &sync.Once{}, + isSync: true, + } +} + // NewModuleInstance implements the k6modules.Module interface to return // a new instance for each VU. func (m *RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance { @@ -78,9 +90,17 @@ func (m *RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance { m.initOnce.Do(func() { m.initialize(vu) }) + + // decide whether to map the browser module to the async JS API or + // the sync one. + mapper := mapBrowserToSobek + if m.isSync { + mapper = syncMapBrowserToSobek + } + return &ModuleInstance{ mod: &JSModule{ - Browser: mapBrowserToGoja(moduleVU{ + Browser: mapper(moduleVU{ VU: vu, pidRegistry: m.PidRegistry, browserRegistry: newBrowserRegistry( diff --git a/vendor/github.com/grafana/xk6-browser/browser/mouse_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/mouse_mapping.go new file mode 100644 index 00000000000..ee5f61b4d3d --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/mouse_mapping.go @@ -0,0 +1,38 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +func mapMouse(vu moduleVU, m *common.Mouse) mapping { + return mapping{ + "click": func(x float64, y float64, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, m.Click(x, y, opts) //nolint:wrapcheck + }) + }, + "dblClick": func(x float64, y float64, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, m.DblClick(x, y, opts) //nolint:wrapcheck + }) + }, + "down": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, m.Down(opts) //nolint:wrapcheck + }) + }, + "up": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, m.Up(opts) //nolint:wrapcheck + }) + }, + "move": func(x float64, y float64, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, m.Move(x, y, opts) //nolint:wrapcheck + }) + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/page_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/page_mapping.go new file mode 100644 index 00000000000..5fadac70069 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/page_mapping.go @@ -0,0 +1,443 @@ +package browser + +import ( + "context" + "fmt" + "time" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapPage to the JS module. +// +//nolint:funlen +func mapPage(vu moduleVU, p *common.Page) mapping { //nolint:gocognit,cyclop + rt := vu.Runtime() + maps := mapping{ + "bringToFront": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.BringToFront() //nolint:wrapcheck + }) + }, + "check": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Check(selector, opts) //nolint:wrapcheck + }) + }, + "click": func(selector string, opts sobek.Value) (*sobek.Promise, error) { + popts, err := parseFrameClickOptions(vu.Context(), opts, p.Timeout()) + if err != nil { + return nil, err + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + err := p.Click(selector, popts) + return nil, err //nolint:wrapcheck + }), nil + }, + "close": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + vu.taskQueueRegistry.close(p.TargetID()) + return nil, p.Close(opts) //nolint:wrapcheck + }) + }, + "content": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.Content() //nolint:wrapcheck + }) + }, + "context": func() mapping { + return mapBrowserContext(vu, p.Context()) + }, + "dblclick": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Dblclick(selector, opts) //nolint:wrapcheck + }) + }, + "dispatchEvent": func(selector, typ string, eventInit, opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameDispatchEventOptions(p.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing page dispatch event options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck + }), nil + }, + "emulateMedia": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.EmulateMedia(opts) //nolint:wrapcheck + }) + }, + "emulateVisionDeficiency": func(typ string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.EmulateVisionDeficiency(typ) //nolint:wrapcheck + }) + }, + "evaluate": func(pageFunction sobek.Value, gargs ...sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.Evaluate(pageFunction.String(), exportArgs(gargs)...) //nolint:wrapcheck + }) + }, + "evaluateHandle": func(pageFunc sobek.Value, gargs ...sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + jsh, err := p.EvaluateHandle(pageFunc.String(), exportArgs(gargs)...) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapJSHandle(vu, jsh), nil + }) + }, + "fill": func(selector string, value string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Fill(selector, value, opts) //nolint:wrapcheck + }) + }, + "focus": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Focus(selector, opts) //nolint:wrapcheck + }) + }, + "frames": func() *sobek.Object { + var ( + mfrs []mapping + frs = p.Frames() + ) + for _, fr := range frs { + mfrs = append(mfrs, mapFrame(vu, fr)) + } + return rt.ToValue(mfrs).ToObject(rt) + }, + "getAttribute": func(selector string, name string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + s, ok, err := p.GetAttribute(selector, name, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil + }) + }, + "goto": func(url string, opts sobek.Value) (*sobek.Promise, error) { + gopts := common.NewFrameGotoOptions( + p.Referrer(), + p.NavigationTimeout(), + ) + if err := gopts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing page navigation options to %q: %w", url, err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + resp, err := p.Goto(url, gopts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + return mapResponse(vu, resp), nil + }), nil + }, + "hover": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Hover(selector, opts) //nolint:wrapcheck + }) + }, + "innerHTML": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.InnerHTML(selector, opts) //nolint:wrapcheck + }) + }, + "innerText": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.InnerText(selector, opts) //nolint:wrapcheck + }) + }, + "inputValue": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.InputValue(selector, opts) //nolint:wrapcheck + }) + }, + "isChecked": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.IsChecked(selector, opts) //nolint:wrapcheck + }) + }, + "isClosed": p.IsClosed, + "isDisabled": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.IsDisabled(selector, opts) //nolint:wrapcheck + }) + }, + "isEditable": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.IsEditable(selector, opts) //nolint:wrapcheck + }) + }, + "isEnabled": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.IsEnabled(selector, opts) //nolint:wrapcheck + }) + }, + "isHidden": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.IsHidden(selector, opts) //nolint:wrapcheck + }) + }, + "isVisible": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.IsVisible(selector, opts) //nolint:wrapcheck + }) + }, + "keyboard": mapKeyboard(vu, p.GetKeyboard()), + "locator": func(selector string, opts sobek.Value) *sobek.Object { + ml := mapLocator(vu, p.Locator(selector, opts)) + return rt.ToValue(ml).ToObject(rt) + }, + "mainFrame": func() *sobek.Object { + mf := mapFrame(vu, p.MainFrame()) + return rt.ToValue(mf).ToObject(rt) + }, + "mouse": mapMouse(vu, p.GetMouse()), + "on": func(event string, handler sobek.Callable) error { + tq := vu.taskQueueRegistry.get(p.TargetID()) + + mapMsgAndHandleEvent := func(m *common.ConsoleMessage) error { + mapping := mapConsoleMessage(vu, m) + _, err := handler(sobek.Undefined(), vu.Runtime().ToValue(mapping)) + return err + } + runInTaskQueue := func(m *common.ConsoleMessage) { + tq.Queue(func() error { + if err := mapMsgAndHandleEvent(m); err != nil { + return fmt.Errorf("executing page.on handler: %w", err) + } + return nil + }) + } + + return p.On(event, runInTaskQueue) //nolint:wrapcheck + }, + "opener": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.Opener(), nil + }) + }, + "press": func(selector string, key string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Press(selector, key, opts) //nolint:wrapcheck + }) + }, + "reload": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + resp, err := p.Reload(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + r := mapResponse(vu, resp) + + return rt.ToValue(r).ToObject(rt), nil + }) + }, + "screenshot": func(opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewPageScreenshotOptions() + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing page screenshot options: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + bb, err := p.Screenshot(popts, vu.filePersister) + if err != nil { + return nil, err //nolint:wrapcheck + } + + ab := rt.NewArrayBuffer(bb) + + return &ab, nil + }), nil + }, + "selectOption": func(selector string, values sobek.Value, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.SelectOption(selector, values, opts) //nolint:wrapcheck + }) + }, + "setContent": func(html string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.SetContent(html, opts) //nolint:wrapcheck + }) + }, + "setDefaultNavigationTimeout": p.SetDefaultNavigationTimeout, + "setDefaultTimeout": p.SetDefaultTimeout, + "setExtraHTTPHeaders": func(headers map[string]string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.SetExtraHTTPHeaders(headers) //nolint:wrapcheck + }) + }, + "setInputFiles": func(selector string, files sobek.Value, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.SetInputFiles(selector, files, opts) //nolint:wrapcheck + }) + }, + "setViewportSize": func(viewportSize sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.SetViewportSize(viewportSize) //nolint:wrapcheck + }) + }, + "tap": func(selector string, opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameTapOptions(p.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing page tap options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Tap(selector, popts) //nolint:wrapcheck + }), nil + }, + "textContent": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + s, ok, err := p.TextContent(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil + }) + }, + "throttleCPU": func(cpuProfile common.CPUProfile) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.ThrottleCPU(cpuProfile) //nolint:wrapcheck + }) + }, + "throttleNetwork": func(networkProfile common.NetworkProfile) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.ThrottleNetwork(networkProfile) //nolint:wrapcheck + }) + }, + "title": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return p.Title() //nolint:wrapcheck + }) + }, + "touchscreen": mapTouchscreen(vu, p.GetTouchscreen()), + "type": func(selector string, text string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Type(selector, text, opts) //nolint:wrapcheck + }) + }, + "uncheck": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Uncheck(selector, opts) //nolint:wrapcheck + }) + }, + "url": p.URL, + "viewportSize": p.ViewportSize, + "waitForFunction": func(pageFunc, opts sobek.Value, args ...sobek.Value) (*sobek.Promise, error) { + js, popts, pargs, err := parseWaitForFunctionArgs( + vu.Context(), p.Timeout(), pageFunc, opts, args..., + ) + if err != nil { + return nil, fmt.Errorf("page waitForFunction: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + return p.WaitForFunction(js, popts, pargs...) //nolint:wrapcheck + }), nil + }, + "waitForLoadState": func(state string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.WaitForLoadState(state, opts) //nolint:wrapcheck + }) + }, + "waitForNavigation": func(opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameWaitForNavigationOptions(p.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing page wait for navigation options: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + resp, err := p.WaitForNavigation(popts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapResponse(vu, resp), nil + }), nil + }, + "waitForSelector": func(selector string, opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + eh, err := p.WaitForSelector(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapElementHandle(vu, eh), nil + }) + }, + "waitForTimeout": func(timeout int64) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + p.WaitForTimeout(timeout) + return nil, nil + }) + }, + "workers": func() *sobek.Object { + var mws []mapping + for _, w := range p.Workers() { + mw := mapWorker(vu, w) + mws = append(mws, mw) + } + return rt.ToValue(mws).ToObject(rt) + }, + } + maps["$"] = func(selector string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + eh, err := p.Query(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + // ElementHandle can be null when the selector does not match any elements. + // We do not want to map nil elementHandles since the expectation is a + // null result in the test script for this case. + if eh == nil { + return nil, nil + } + ehm := mapElementHandle(vu, eh) + + return ehm, nil + }) + } + maps["$$"] = func(selector string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + ehs, err := p.QueryAll(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + var mehs []mapping + for _, eh := range ehs { + ehm := mapElementHandle(vu, eh) + mehs = append(mehs, ehm) + } + return mehs, nil + }) + } + + return maps +} + +func parseWaitForFunctionArgs( + ctx context.Context, timeout time.Duration, pageFunc, opts sobek.Value, gargs ...sobek.Value, +) (string, *common.FrameWaitForFunctionOptions, []any, error) { + popts := common.NewFrameWaitForFunctionOptions(timeout) + err := popts.Parse(ctx, opts) + if err != nil { + return "", nil, nil, fmt.Errorf("parsing waitForFunction options: %w", err) + } + + js := pageFunc.ToString().String() + _, isCallable := sobek.AssertFunction(pageFunc) + if !isCallable { + js = fmt.Sprintf("() => (%s)", js) + } + + return js, popts, exportArgs(gargs), nil +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/request_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/request_mapping.go new file mode 100644 index 00000000000..a5d714f4b9d --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/request_mapping.go @@ -0,0 +1,66 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapRequest to the JS module. +func mapRequest(vu moduleVU, r *common.Request) mapping { + rt := vu.Runtime() + maps := mapping{ + "allHeaders": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.AllHeaders(), nil + }) + }, + "frame": func() *sobek.Object { + mf := mapFrame(vu, r.Frame()) + return rt.ToValue(mf).ToObject(rt) + }, + "headerValue": func(name string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.HeaderValue(name), nil + }) + }, + "headers": r.Headers, + "headersArray": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.HeadersArray(), nil + }) + }, + "isNavigationRequest": r.IsNavigationRequest, + "method": r.Method, + "postData": func() any { + p := r.PostData() + if p == "" { + return nil + } + return p + }, + "postDataBuffer": func() any { + p := r.PostDataBuffer() + if len(p) == 0 { + return nil + } + return rt.NewArrayBuffer(p) + }, + "resourceType": r.ResourceType, + "response": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + resp := r.Response() + if resp == nil { + return nil, nil + } + return mapResponse(vu, resp), nil + }) + }, + "size": r.Size, + "timing": r.Timing, + "url": r.URL, + } + + return maps +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/response_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/response_mapping.go new file mode 100644 index 00000000000..6bc43756516 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/response_mapping.go @@ -0,0 +1,89 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapResponse to the JS module. +func mapResponse(vu moduleVU, r *common.Response) mapping { //nolint:funlen + if r == nil { + return nil + } + maps := mapping{ + "allHeaders": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.AllHeaders(), nil + }) + }, + "body": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + body, err := r.Body() + if err != nil { + return nil, err //nolint: wrapcheck + } + buf := vu.Runtime().NewArrayBuffer(body) + return &buf, nil + }) + }, + "frame": func() mapping { + return mapFrame(vu, r.Frame()) + }, + "headerValue": func(name string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + v, ok := r.HeaderValue(name) + if !ok { + return nil, nil + } + return v, nil + }) + }, + "headerValues": func(name string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.HeaderValues(name), nil + }) + }, + "headers": r.Headers, + "headersArray": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.HeadersArray(), nil + }) + }, + "json": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.JSON() //nolint: wrapcheck + }) + }, + "ok": r.Ok, + "request": func() mapping { + return mapRequest(vu, r.Request()) + }, + "securityDetails": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.SecurityDetails(), nil + }) + }, + "serverAddr": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.ServerAddr(), nil + }) + }, + "size": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.Size(), nil + }) + }, + "status": r.Status, + "statusText": r.StatusText, + "url": r.URL, + "text": func() *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return r.Text() //nolint:wrapcheck + }) + }, + } + + return maps +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_browser_context_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_browser_context_mapping.go new file mode 100644 index 00000000000..5e8de66d4e3 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_browser_context_mapping.go @@ -0,0 +1,132 @@ +package browser + +import ( + "fmt" + "reflect" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6error" + "github.com/grafana/xk6-browser/k6ext" +) + +// syncMapBrowserContext is like mapBrowserContext but returns synchronous functions. +func syncMapBrowserContext(vu moduleVU, bc *common.BrowserContext) mapping { //nolint:funlen,gocognit,cyclop + rt := vu.Runtime() + return mapping{ + "addCookies": bc.AddCookies, + "addInitScript": func(script sobek.Value) error { + if !sobekValueExists(script) { + return nil + } + + source := "" + switch script.ExportType() { + case reflect.TypeOf(string("")): + source = script.String() + case reflect.TypeOf(sobek.Object{}): + opts := script.ToObject(rt) + for _, k := range opts.Keys() { + if k == "content" { + source = opts.Get(k).String() + } + } + default: + _, isCallable := sobek.AssertFunction(script) + if !isCallable { + source = fmt.Sprintf("(%s);", script.ToString().String()) + } else { + source = fmt.Sprintf("(%s)(...args);", script.ToString().String()) + } + } + + return bc.AddInitScript(source) //nolint:wrapcheck + }, + "browser": bc.Browser, + "clearCookies": bc.ClearCookies, + "clearPermissions": bc.ClearPermissions, + "close": bc.Close, + "cookies": bc.Cookies, + "grantPermissions": func(permissions []string, opts sobek.Value) error { + pOpts := common.NewGrantPermissionsOptions() + pOpts.Parse(vu.Context(), opts) + + return bc.GrantPermissions(permissions, pOpts) //nolint:wrapcheck + }, + "setDefaultNavigationTimeout": bc.SetDefaultNavigationTimeout, + "setDefaultTimeout": bc.SetDefaultTimeout, + "setGeolocation": bc.SetGeolocation, + "setHTTPCredentials": bc.SetHTTPCredentials, //nolint:staticcheck + "setOffline": bc.SetOffline, + "waitForEvent": func(event string, optsOrPredicate sobek.Value) (*sobek.Promise, error) { + ctx := vu.Context() + popts := common.NewWaitForEventOptions( + bc.Timeout(), + ) + if err := popts.Parse(ctx, optsOrPredicate); err != nil { + return nil, fmt.Errorf("parsing waitForEvent options: %w", err) + } + + return k6ext.Promise(ctx, func() (result any, reason error) { + var runInTaskQueue func(p *common.Page) (bool, error) + if popts.PredicateFn != nil { + runInTaskQueue = func(p *common.Page) (bool, error) { + tq := vu.taskQueueRegistry.get(p.TargetID()) + + var rtn bool + var err error + // The function on the taskqueue runs in its own goroutine + // so we need to use a channel to wait for it to complete + // before returning the result to the caller. + c := make(chan bool) + tq.Queue(func() error { + var resp sobek.Value + resp, err = popts.PredicateFn(vu.Runtime().ToValue(p)) + rtn = resp.ToBoolean() + close(c) + return nil + }) + <-c + + return rtn, err //nolint:wrapcheck + } + } + + resp, err := bc.WaitForEvent(event, runInTaskQueue, popts.Timeout) + panicIfFatalError(ctx, err) + if err != nil { + return nil, err //nolint:wrapcheck + } + p, ok := resp.(*common.Page) + if !ok { + panicIfFatalError(ctx, fmt.Errorf("response object is not a page: %w", k6error.ErrFatal)) + } + + return syncMapPage(vu, p), nil + }), nil + }, + "pages": func() *sobek.Object { + var ( + mpages []mapping + pages = bc.Pages() + ) + for _, page := range pages { + if page == nil { + continue + } + m := syncMapPage(vu, page) + mpages = append(mpages, m) + } + + return rt.ToValue(mpages).ToObject(rt) + }, + "newPage": func() (mapping, error) { + page, err := bc.NewPage() + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapPage(vu, page), nil + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_browser_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_browser_mapping.go new file mode 100644 index 00000000000..400e31ca64b --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_browser_mapping.go @@ -0,0 +1,81 @@ +package browser + +import ( + "github.com/grafana/sobek" +) + +// syncMapBrowser is like mapBrowser but returns synchronous functions. +func syncMapBrowser(vu moduleVU) mapping { //nolint:funlen,cyclop + rt := vu.Runtime() + return mapping{ + "context": func() (mapping, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } + return syncMapBrowserContext(vu, b.Context()), nil + }, + "closeContext": func() error { + b, err := vu.browser() + if err != nil { + return err + } + return b.CloseContext() //nolint:wrapcheck + }, + "isConnected": func() (bool, error) { + b, err := vu.browser() + if err != nil { + return false, err + } + return b.IsConnected(), nil + }, + "newContext": func(opts sobek.Value) (*sobek.Object, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } + bctx, err := b.NewContext(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + if err := initBrowserContext(bctx, vu.testRunID); err != nil { + return nil, err + } + + m := syncMapBrowserContext(vu, bctx) + + return rt.ToValue(m).ToObject(rt), nil + }, + "userAgent": func() (string, error) { + b, err := vu.browser() + if err != nil { + return "", err + } + return b.UserAgent(), nil + }, + "version": func() (string, error) { + b, err := vu.browser() + if err != nil { + return "", err + } + return b.Version(), nil + }, + "newPage": func(opts sobek.Value) (mapping, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } + page, err := b.NewPage(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + if err := initBrowserContext(b.Context(), vu.testRunID); err != nil { + return nil, err + } + + return syncMapPage(vu, page), nil + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_console_message_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_console_message_mapping.go new file mode 100644 index 00000000000..6884c2a0873 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_console_message_mapping.go @@ -0,0 +1,31 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" +) + +// syncMapConsoleMessage is like mapConsoleMessage but returns synchronous functions. +func syncMapConsoleMessage(vu moduleVU, cm *common.ConsoleMessage) mapping { + rt := vu.Runtime() + return mapping{ + "args": func() *sobek.Object { + var ( + margs []mapping + args = cm.Args + ) + for _, arg := range args { + a := syncMapJSHandle(vu, arg) + margs = append(margs, a) + } + + return rt.ToValue(margs).ToObject(rt) + }, + // page(), text() and type() are defined as + // functions in order to match Playwright's API + "page": func() mapping { return syncMapPage(vu, cm.Page) }, + "text": func() string { return cm.Text }, + "type": func() string { return cm.Type }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_element_handle_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_element_handle_mapping.go new file mode 100644 index 00000000000..e9375f10011 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_element_handle_mapping.go @@ -0,0 +1,157 @@ +package browser + +import ( + "fmt" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// syncMapElementHandle is like mapElementHandle but returns synchronous functions. +func syncMapElementHandle(vu moduleVU, eh *common.ElementHandle) mapping { //nolint:gocognit,cyclop,funlen + rt := vu.Runtime() + maps := mapping{ + "boundingBox": eh.BoundingBox, + "check": eh.Check, + "click": func(opts sobek.Value) (*sobek.Promise, error) { + ctx := vu.Context() + + popts := common.NewElementHandleClickOptions(eh.Timeout()) + if err := popts.Parse(ctx, opts); err != nil { + return nil, fmt.Errorf("parsing element click options: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + err := eh.Click(popts) + return nil, err //nolint:wrapcheck + }), nil + }, + "contentFrame": func() (mapping, error) { + f, err := eh.ContentFrame() + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapFrame(vu, f), nil + }, + "dblclick": eh.Dblclick, + "dispatchEvent": func(typ string, eventInit sobek.Value) error { + return eh.DispatchEvent(typ, exportArg(eventInit)) //nolint:wrapcheck + }, + "fill": eh.Fill, + "focus": eh.Focus, + "getAttribute": func(name string) (any, error) { + v, ok, err := eh.GetAttribute(name) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil //nolint:nilnil + } + return v, nil + }, + "hover": eh.Hover, + "innerHTML": eh.InnerHTML, + "innerText": eh.InnerText, + "inputValue": eh.InputValue, + "isChecked": eh.IsChecked, + "isDisabled": eh.IsDisabled, + "isEditable": eh.IsEditable, + "isEnabled": eh.IsEnabled, + "isHidden": eh.IsHidden, + "isVisible": eh.IsVisible, + "ownerFrame": func() (mapping, error) { + f, err := eh.OwnerFrame() + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapFrame(vu, f), nil + }, + "press": eh.Press, + "screenshot": func(opts sobek.Value) (*sobek.ArrayBuffer, error) { + ctx := vu.Context() + + popts := common.NewElementHandleScreenshotOptions(eh.Timeout()) + if err := popts.Parse(ctx, opts); err != nil { + return nil, fmt.Errorf("parsing frame screenshot options: %w", err) + } + + bb, err := eh.Screenshot(popts, vu.filePersister) + if err != nil { + return nil, err //nolint:wrapcheck + } + + ab := rt.NewArrayBuffer(bb) + + return &ab, nil + }, + "scrollIntoViewIfNeeded": eh.ScrollIntoViewIfNeeded, + "selectOption": eh.SelectOption, + "selectText": eh.SelectText, + "setInputFiles": eh.SetInputFiles, + "tap": func(opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewElementHandleTapOptions(eh.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing element tap options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, eh.Tap(popts) //nolint:wrapcheck + }), nil + }, + "textContent": func() (any, error) { + v, ok, err := eh.TextContent() + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil //nolint:nilnil + } + return v, nil + }, + "type": eh.Type, + "uncheck": eh.Uncheck, + "waitForElementState": eh.WaitForElementState, + "waitForSelector": func(selector string, opts sobek.Value) (mapping, error) { + eh, err := eh.WaitForSelector(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapElementHandle(vu, eh), nil + }, + } + maps["$"] = func(selector string) (mapping, error) { + eh, err := eh.Query(selector, common.StrictModeOff) + if err != nil { + return nil, err //nolint:wrapcheck + } + // ElementHandle can be null when the selector does not match any elements. + // We do not want to map nil elementHandles since the expectation is a + // null result in the test script for this case. + if eh == nil { + return nil, nil //nolint:nilnil + } + ehm := syncMapElementHandle(vu, eh) + + return ehm, nil + } + maps["$$"] = func(selector string) ([]mapping, error) { + ehs, err := eh.QueryAll(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + var mehs []mapping + for _, eh := range ehs { + ehm := syncMapElementHandle(vu, eh) + mehs = append(mehs, ehm) + } + return mehs, nil + } + + jsHandleMap := syncMapJSHandle(vu, eh) + for k, v := range jsHandleMap { + maps[k] = v + } + + return maps +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_frame_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_frame_mapping.go new file mode 100644 index 00000000000..0cde428594c --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_frame_mapping.go @@ -0,0 +1,209 @@ +package browser + +import ( + "fmt" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// syncMapFrame is like mapFrame but returns synchronous functions. +func syncMapFrame(vu moduleVU, f *common.Frame) mapping { //nolint:gocognit,cyclop,funlen + rt := vu.Runtime() + maps := mapping{ + "check": f.Check, + "childFrames": func() *sobek.Object { + var ( + mcfs []mapping + cfs = f.ChildFrames() + ) + for _, fr := range cfs { + mcfs = append(mcfs, syncMapFrame(vu, fr)) + } + return rt.ToValue(mcfs).ToObject(rt) + }, + "click": func(selector string, opts sobek.Value) (*sobek.Promise, error) { + popts, err := parseFrameClickOptions(vu.Context(), opts, f.Timeout()) + if err != nil { + return nil, err + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + err := f.Click(selector, popts) + return nil, err //nolint:wrapcheck + }), nil + }, + "content": f.Content, + "dblclick": f.Dblclick, + "dispatchEvent": func(selector, typ string, eventInit, opts sobek.Value) error { + popts := common.NewFrameDispatchEventOptions(f.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return fmt.Errorf("parsing frame dispatch event options: %w", err) + } + return f.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck + }, + "evaluate": func(pageFunction sobek.Value, gargs ...sobek.Value) (any, error) { + return f.Evaluate(pageFunction.String(), exportArgs(gargs)...) //nolint:wrapcheck + }, + "evaluateHandle": func(pageFunction sobek.Value, gargs ...sobek.Value) (mapping, error) { + jsh, err := f.EvaluateHandle(pageFunction.String(), exportArgs(gargs)...) + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapJSHandle(vu, jsh), nil + }, + "fill": f.Fill, + "focus": f.Focus, + "frameElement": func() (mapping, error) { + fe, err := f.FrameElement() + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapElementHandle(vu, fe), nil + }, + "getAttribute": func(selector, name string, opts sobek.Value) (any, error) { + v, ok, err := f.GetAttribute(selector, name, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil //nolint:nilnil + } + return v, nil + }, + "goto": func(url string, opts sobek.Value) (*sobek.Promise, error) { + gopts := common.NewFrameGotoOptions( + f.Referrer(), + f.NavigationTimeout(), + ) + if err := gopts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing frame navigation options to %q: %w", url, err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + resp, err := f.Goto(url, gopts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + return syncMapResponse(vu, resp), nil + }), nil + }, + "hover": f.Hover, + "innerHTML": f.InnerHTML, + "innerText": f.InnerText, + "inputValue": f.InputValue, + "isChecked": f.IsChecked, + "isDetached": f.IsDetached, + "isDisabled": f.IsDisabled, + "isEditable": f.IsEditable, + "isEnabled": f.IsEnabled, + "isHidden": f.IsHidden, + "isVisible": f.IsVisible, + "locator": func(selector string, opts sobek.Value) *sobek.Object { + ml := syncMapLocator(vu, f.Locator(selector, opts)) + return rt.ToValue(ml).ToObject(rt) + }, + "name": f.Name, + "page": func() *sobek.Object { + mp := syncMapPage(vu, f.Page()) + return rt.ToValue(mp).ToObject(rt) + }, + "parentFrame": func() *sobek.Object { + mf := syncMapFrame(vu, f.ParentFrame()) + return rt.ToValue(mf).ToObject(rt) + }, + "press": f.Press, + "selectOption": f.SelectOption, + "setContent": f.SetContent, + "setInputFiles": f.SetInputFiles, + "tap": func(selector string, opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameTapOptions(f.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing frame tap options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, f.Tap(selector, popts) //nolint:wrapcheck + }), nil + }, + "textContent": func(selector string, opts sobek.Value) (any, error) { + v, ok, err := f.TextContent(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil //nolint:nilnil + } + return v, nil + }, + "title": f.Title, + "type": f.Type, + "uncheck": f.Uncheck, + "url": f.URL, + "waitForFunction": func(pageFunc, opts sobek.Value, args ...sobek.Value) (*sobek.Promise, error) { + js, popts, pargs, err := parseWaitForFunctionArgs( + vu.Context(), f.Timeout(), pageFunc, opts, args..., + ) + if err != nil { + return nil, fmt.Errorf("frame waitForFunction: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + return f.WaitForFunction(js, popts, pargs...) //nolint:wrapcheck + }), nil + }, + "waitForLoadState": f.WaitForLoadState, + "waitForNavigation": func(opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameWaitForNavigationOptions(f.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing frame wait for navigation options: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + resp, err := f.WaitForNavigation(popts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapResponse(vu, resp), nil + }), nil + }, + "waitForSelector": func(selector string, opts sobek.Value) (mapping, error) { + eh, err := f.WaitForSelector(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapElementHandle(vu, eh), nil + }, + "waitForTimeout": f.WaitForTimeout, + } + maps["$"] = func(selector string) (mapping, error) { + eh, err := f.Query(selector, common.StrictModeOff) + if err != nil { + return nil, err //nolint:wrapcheck + } + // ElementHandle can be null when the selector does not match any elements. + // We do not want to map nil elementHandles since the expectation is a + // null result in the test script for this case. + if eh == nil { + return nil, nil //nolint:nilnil + } + ehm := syncMapElementHandle(vu, eh) + + return ehm, nil + } + maps["$$"] = func(selector string) ([]mapping, error) { + ehs, err := f.QueryAll(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + var mehs []mapping + for _, eh := range ehs { + ehm := syncMapElementHandle(vu, eh) + mehs = append(mehs, ehm) + } + return mehs, nil + } + + return maps +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_js_handle_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_js_handle_mapping.go new file mode 100644 index 00000000000..dda134547d5 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_js_handle_mapping.go @@ -0,0 +1,46 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" +) + +// syncMapJSHandle is like mapJSHandle but returns synchronous functions. +func syncMapJSHandle(vu moduleVU, jsh common.JSHandleAPI) mapping { + rt := vu.Runtime() + return mapping{ + "asElement": func() *sobek.Object { + m := syncMapElementHandle(vu, jsh.AsElement()) + return rt.ToValue(m).ToObject(rt) + }, + "dispose": jsh.Dispose, + "evaluate": func(pageFunc sobek.Value, gargs ...sobek.Value) (any, error) { + args := make([]any, 0, len(gargs)) + for _, a := range gargs { + args = append(args, exportArg(a)) + } + return jsh.Evaluate(pageFunc.String(), args...) //nolint:wrapcheck + }, + "evaluateHandle": func(pageFunc sobek.Value, gargs ...sobek.Value) (mapping, error) { + h, err := jsh.EvaluateHandle(pageFunc.String(), exportArgs(gargs)...) + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapJSHandle(vu, h), nil + }, + "getProperties": func() (mapping, error) { + props, err := jsh.GetProperties() + if err != nil { + return nil, err //nolint:wrapcheck + } + + dst := make(map[string]any) + for k, v := range props { + dst[k] = syncMapJSHandle(vu, v) + } + return dst, nil + }, + "jsonValue": jsh.JSONValue, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_locator_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_locator_mapping.go new file mode 100644 index 00000000000..fa15bf502dc --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_locator_mapping.go @@ -0,0 +1,91 @@ +package browser + +import ( + "fmt" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// syncMapLocator is like mapLocator but returns synchronous functions. +func syncMapLocator(vu moduleVU, lo *common.Locator) mapping { //nolint:funlen + return mapping{ + "clear": func(opts sobek.Value) error { + ctx := vu.Context() + + copts := common.NewFrameFillOptions(lo.Timeout()) + if err := copts.Parse(ctx, opts); err != nil { + return fmt.Errorf("parsing clear options: %w", err) + } + + return lo.Clear(copts) //nolint:wrapcheck + }, + "click": func(opts sobek.Value) (*sobek.Promise, error) { + popts, err := parseFrameClickOptions(vu.Context(), opts, lo.Timeout()) + if err != nil { + return nil, err + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Click(popts) //nolint:wrapcheck + }), nil + }, + "dblclick": lo.Dblclick, + "check": lo.Check, + "uncheck": lo.Uncheck, + "isChecked": lo.IsChecked, + "isEditable": lo.IsEditable, + "isEnabled": lo.IsEnabled, + "isDisabled": lo.IsDisabled, + "isVisible": lo.IsVisible, + "isHidden": lo.IsHidden, + "fill": lo.Fill, + "focus": lo.Focus, + "getAttribute": func(name string, opts sobek.Value) (any, error) { + v, ok, err := lo.GetAttribute(name, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return v, nil + }, + "innerHTML": lo.InnerHTML, + "innerText": lo.InnerText, + "textContent": func(opts sobek.Value) (any, error) { + v, ok, err := lo.TextContent(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return v, nil + }, + "inputValue": lo.InputValue, + "selectOption": lo.SelectOption, + "press": lo.Press, + "type": lo.Type, + "hover": lo.Hover, + "tap": func(opts sobek.Value) (*sobek.Promise, error) { + copts := common.NewFrameTapOptions(lo.DefaultTimeout()) + if err := copts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing locator tap options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, lo.Tap(copts) //nolint:wrapcheck + }), nil + }, + "dispatchEvent": func(typ string, eventInit, opts sobek.Value) error { + popts := common.NewFrameDispatchEventOptions(lo.DefaultTimeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return fmt.Errorf("parsing locator dispatch event options: %w", err) + } + return lo.DispatchEvent(typ, exportArg(eventInit), popts) //nolint:wrapcheck + }, + "waitFor": lo.WaitFor, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_mapping.go new file mode 100644 index 00000000000..137132be255 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_mapping.go @@ -0,0 +1,26 @@ +package browser + +import ( + "fmt" + + "github.com/grafana/sobek" + + k6common "go.k6.io/k6/js/common" +) + +// syncMapBrowserToSobek maps the browser API to the JS module as a +// synchronous version. +func syncMapBrowserToSobek(vu moduleVU) *sobek.Object { + var ( + rt = vu.Runtime() + obj = rt.NewObject() + ) + for k, v := range syncMapBrowser(vu) { + err := obj.Set(k, rt.ToValue(v)) + if err != nil { + k6common.Throw(rt, fmt.Errorf("mapping: %w", err)) + } + } + + return obj +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_page_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_page_mapping.go new file mode 100644 index 00000000000..19a6a7a7f09 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_page_mapping.go @@ -0,0 +1,271 @@ +package browser + +import ( + "fmt" + + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// syncMapPage is like mapPage but returns synchronous functions. +func syncMapPage(vu moduleVU, p *common.Page) mapping { //nolint:gocognit,cyclop,funlen + rt := vu.Runtime() + maps := mapping{ + "bringToFront": p.BringToFront, + "check": p.Check, + "click": func(selector string, opts sobek.Value) (*sobek.Promise, error) { + popts, err := parseFrameClickOptions(vu.Context(), opts, p.Timeout()) + if err != nil { + return nil, err + } + + return k6ext.Promise(vu.Context(), func() (any, error) { + err := p.Click(selector, popts) + return nil, err //nolint:wrapcheck + }), nil + }, + "close": func(opts sobek.Value) error { + vu.taskQueueRegistry.close(p.TargetID()) + + return p.Close(opts) //nolint:wrapcheck + }, + "content": p.Content, + "context": p.Context, + "dblclick": p.Dblclick, + "dispatchEvent": func(selector, typ string, eventInit, opts sobek.Value) error { + popts := common.NewFrameDispatchEventOptions(p.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return fmt.Errorf("parsing page dispatch event options: %w", err) + } + return p.DispatchEvent(selector, typ, exportArg(eventInit), popts) //nolint:wrapcheck + }, + "emulateMedia": p.EmulateMedia, + "emulateVisionDeficiency": p.EmulateVisionDeficiency, + "evaluate": func(pageFunction sobek.Value, gargs ...sobek.Value) (any, error) { + return p.Evaluate(pageFunction.String(), exportArgs(gargs)...) //nolint:wrapcheck + }, + "evaluateHandle": func(pageFunc sobek.Value, gargs ...sobek.Value) (mapping, error) { + jsh, err := p.EvaluateHandle(pageFunc.String(), exportArgs(gargs)...) + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapJSHandle(vu, jsh), nil + }, + "fill": p.Fill, + "focus": p.Focus, + "frames": func() *sobek.Object { + var ( + mfrs []mapping + frs = p.Frames() + ) + for _, fr := range frs { + mfrs = append(mfrs, syncMapFrame(vu, fr)) + } + return rt.ToValue(mfrs).ToObject(rt) + }, + "getAttribute": func(selector string, name string, opts sobek.Value) (any, error) { + v, ok, err := p.GetAttribute(selector, name, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil //nolint:nilnil + } + return v, nil + }, + "goto": func(url string, opts sobek.Value) (*sobek.Promise, error) { + gopts := common.NewFrameGotoOptions( + p.Referrer(), + p.NavigationTimeout(), + ) + if err := gopts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing page navigation options to %q: %w", url, err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + resp, err := p.Goto(url, gopts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + return syncMapResponse(vu, resp), nil + }), nil + }, + "hover": p.Hover, + "innerHTML": p.InnerHTML, + "innerText": p.InnerText, + "inputValue": p.InputValue, + "isChecked": p.IsChecked, + "isClosed": p.IsClosed, + "isDisabled": p.IsDisabled, + "isEditable": p.IsEditable, + "isEnabled": p.IsEnabled, + "isHidden": p.IsHidden, + "isVisible": p.IsVisible, + "keyboard": rt.ToValue(p.GetKeyboard()).ToObject(rt), + "locator": func(selector string, opts sobek.Value) *sobek.Object { + ml := syncMapLocator(vu, p.Locator(selector, opts)) + return rt.ToValue(ml).ToObject(rt) + }, + "mainFrame": func() *sobek.Object { + mf := syncMapFrame(vu, p.MainFrame()) + return rt.ToValue(mf).ToObject(rt) + }, + "mouse": rt.ToValue(p.GetMouse()).ToObject(rt), + "on": func(event string, handler sobek.Callable) error { + tq := vu.taskQueueRegistry.get(p.TargetID()) + + mapMsgAndHandleEvent := func(m *common.ConsoleMessage) error { + mapping := syncMapConsoleMessage(vu, m) + _, err := handler(sobek.Undefined(), vu.Runtime().ToValue(mapping)) + return err + } + runInTaskQueue := func(m *common.ConsoleMessage) { + tq.Queue(func() error { + if err := mapMsgAndHandleEvent(m); err != nil { + return fmt.Errorf("executing page.on handler: %w", err) + } + return nil + }) + } + + return p.On(event, runInTaskQueue) //nolint:wrapcheck + }, + "opener": p.Opener, + "press": p.Press, + "reload": func(opts sobek.Value) (*sobek.Object, error) { + resp, err := p.Reload(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + + r := syncMapResponse(vu, resp) + + return rt.ToValue(r).ToObject(rt), nil + }, + "screenshot": func(opts sobek.Value) (*sobek.ArrayBuffer, error) { + ctx := vu.Context() + + popts := common.NewPageScreenshotOptions() + if err := popts.Parse(ctx, opts); err != nil { + return nil, fmt.Errorf("parsing page screenshot options: %w", err) + } + + bb, err := p.Screenshot(popts, vu.filePersister) + if err != nil { + return nil, err //nolint:wrapcheck + } + + ab := rt.NewArrayBuffer(bb) + + return &ab, nil + }, + "selectOption": p.SelectOption, + "setContent": p.SetContent, + "setDefaultNavigationTimeout": p.SetDefaultNavigationTimeout, + "setDefaultTimeout": p.SetDefaultTimeout, + "setExtraHTTPHeaders": p.SetExtraHTTPHeaders, + "setInputFiles": p.SetInputFiles, + "setViewportSize": p.SetViewportSize, + "tap": func(selector string, opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameTapOptions(p.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing page tap options: %w", err) + } + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, p.Tap(selector, popts) //nolint:wrapcheck + }), nil + }, + "textContent": func(selector string, opts sobek.Value) (any, error) { + v, ok, err := p.TextContent(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil //nolint:nilnil + } + return v, nil + }, + "throttleCPU": p.ThrottleCPU, + "throttleNetwork": p.ThrottleNetwork, + "title": p.Title, + "touchscreen": syncMapTouchscreen(vu, p.GetTouchscreen()), + "type": p.Type, + "uncheck": p.Uncheck, + "url": p.URL, + "viewportSize": p.ViewportSize, + "waitForFunction": func(pageFunc, opts sobek.Value, args ...sobek.Value) (*sobek.Promise, error) { + js, popts, pargs, err := parseWaitForFunctionArgs( + vu.Context(), p.Timeout(), pageFunc, opts, args..., + ) + if err != nil { + return nil, fmt.Errorf("page waitForFunction: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + return p.WaitForFunction(js, popts, pargs...) //nolint:wrapcheck + }), nil + }, + "waitForLoadState": p.WaitForLoadState, + "waitForNavigation": func(opts sobek.Value) (*sobek.Promise, error) { + popts := common.NewFrameWaitForNavigationOptions(p.Timeout()) + if err := popts.Parse(vu.Context(), opts); err != nil { + return nil, fmt.Errorf("parsing page wait for navigation options: %w", err) + } + + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + resp, err := p.WaitForNavigation(popts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapResponse(vu, resp), nil + }), nil + }, + "waitForSelector": func(selector string, opts sobek.Value) (mapping, error) { + eh, err := p.WaitForSelector(selector, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return syncMapElementHandle(vu, eh), nil + }, + "waitForTimeout": p.WaitForTimeout, + "workers": func() *sobek.Object { + var mws []mapping + for _, w := range p.Workers() { + mw := syncMapWorker(vu, w) + mws = append(mws, mw) + } + return rt.ToValue(mws).ToObject(rt) + }, + } + maps["$"] = func(selector string) (mapping, error) { + eh, err := p.Query(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + // ElementHandle can be null when the selector does not match any elements. + // We do not want to map nil elementHandles since the expectation is a + // null result in the test script for this case. + if eh == nil { + return nil, nil //nolint:nilnil + } + ehm := syncMapElementHandle(vu, eh) + + return ehm, nil + } + maps["$$"] = func(selector string) ([]mapping, error) { + ehs, err := p.QueryAll(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + var mehs []mapping + for _, eh := range ehs { + ehm := syncMapElementHandle(vu, eh) + mehs = append(mehs, ehm) + } + return mehs, nil + } + + return maps +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_request_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_request_mapping.go new file mode 100644 index 00000000000..abc6337d3ab --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_request_mapping.go @@ -0,0 +1,27 @@ +package browser + +import ( + "github.com/grafana/xk6-browser/common" +) + +// syncMapRequest is like mapRequest but returns synchronous functions. +func syncMapRequest(vu moduleVU, r *common.Request) mapping { + maps := mapping{ + "allHeaders": r.AllHeaders, + "frame": func() mapping { return syncMapFrame(vu, r.Frame()) }, + "headerValue": r.HeaderValue, + "headers": r.Headers, + "headersArray": r.HeadersArray, + "isNavigationRequest": r.IsNavigationRequest, + "method": r.Method, + "postData": r.PostData, + "postDataBuffer": r.PostDataBuffer, + "resourceType": r.ResourceType, + "response": func() mapping { return syncMapResponse(vu, r.Response()) }, + "size": r.Size, + "timing": r.Timing, + "url": r.URL, + } + + return maps +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_response_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_response_mapping.go new file mode 100644 index 00000000000..3b564376e2c --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_response_mapping.go @@ -0,0 +1,32 @@ +package browser + +import ( + "github.com/grafana/xk6-browser/common" +) + +// syncMapResponse is like mapResponse but returns synchronous functions. +func syncMapResponse(vu moduleVU, r *common.Response) mapping { + if r == nil { + return nil + } + maps := mapping{ + "allHeaders": r.AllHeaders, + "body": r.Body, + "frame": func() mapping { return syncMapFrame(vu, r.Frame()) }, + "headerValue": r.HeaderValue, + "headerValues": r.HeaderValues, + "headers": r.Headers, + "headersArray": r.HeadersArray, + "json": r.JSON, + "ok": r.Ok, + "request": func() mapping { return syncMapRequest(vu, r.Request()) }, + "securityDetails": r.SecurityDetails, + "serverAddr": r.ServerAddr, + "size": r.Size, + "status": r.Status, + "statusText": r.StatusText, + "url": r.URL, + } + + return maps +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_touchscreen_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_touchscreen_mapping.go new file mode 100644 index 00000000000..88537dc3669 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_touchscreen_mapping.go @@ -0,0 +1,19 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// syncMapTouchscreen is like mapTouchscreen but returns synchronous functions. +func syncMapTouchscreen(vu moduleVU, ts *common.Touchscreen) mapping { + return mapping{ + "tap": func(x float64, y float64) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + return nil, ts.Tap(x, y) //nolint:wrapcheck + }) + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/sync_worker_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/sync_worker_mapping.go new file mode 100644 index 00000000000..a2a6c005fd4 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/sync_worker_mapping.go @@ -0,0 +1,12 @@ +package browser + +import ( + "github.com/grafana/xk6-browser/common" +) + +// syncMapWorker is like mapWorker but returns synchronous functions. +func syncMapWorker(_ moduleVU, w *common.Worker) mapping { + return mapping{ + "url": w.URL(), + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/touchscreen_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/touchscreen_mapping.go new file mode 100644 index 00000000000..ea18f95dba8 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/touchscreen_mapping.go @@ -0,0 +1,19 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/k6ext" +) + +// mapTouchscreen to the JS module. +func mapTouchscreen(vu moduleVU, ts *common.Touchscreen) mapping { + return mapping{ + "tap": func(x float64, y float64) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (result any, reason error) { + return nil, ts.Tap(x, y) //nolint:wrapcheck + }) + }, + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/worker_mapping.go b/vendor/github.com/grafana/xk6-browser/browser/worker_mapping.go new file mode 100644 index 00000000000..0e944ae641c --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/worker_mapping.go @@ -0,0 +1,12 @@ +package browser + +import ( + "github.com/grafana/xk6-browser/common" +) + +// mapWorker to the JS module. +func mapWorker(_ moduleVU, w *common.Worker) mapping { + return mapping{ + "url": w.URL(), + } +} diff --git a/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go b/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go index 40467fa8680..ed3c2f4b4d7 100644 --- a/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go +++ b/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go @@ -36,7 +36,7 @@ type BrowserType struct { } // NewBrowserType registers our custom k6 metrics, creates method mappings on -// the goja runtime, and returns a new Chrome browser type. +// the sobek runtime, and returns a new Chrome browser type. func NewBrowserType(vu k6modules.VU) *BrowserType { // NOTE: vu.InitEnv() *must* be called from the script init scope, // otherwise it will return nil. diff --git a/vendor/github.com/grafana/xk6-browser/common/browser.go b/vendor/github.com/grafana/xk6-browser/common/browser.go index bcb320f3c00..c1b9bc98c3b 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser.go @@ -9,17 +9,17 @@ import ( "sync/atomic" "time" - "github.com/grafana/xk6-browser/k6ext" - "github.com/grafana/xk6-browser/log" - - k6modules "go.k6.io/k6/js/modules" - "github.com/chromedp/cdproto" cdpbrowser "github.com/chromedp/cdproto/browser" "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/target" - "github.com/dop251/goja" "github.com/gorilla/websocket" + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/k6ext" + "github.com/grafana/xk6-browser/log" + + k6modules "go.k6.io/k6/js/modules" ) const ( @@ -68,11 +68,23 @@ type Browser struct { // Used to display a warning when the browser is reclosed. closed bool + // version caches the browser version information. + version browserVersion + vu k6modules.VU logger *log.Logger } +// browserVersion is a struct to hold the browser version information. +type browserVersion struct { + protocolVersion string + product string + revision string + userAgent string + jsVersion string +} + // NewBrowser creates a new browser, connects to it, then returns it. func NewBrowser( ctx context.Context, @@ -85,6 +97,13 @@ func NewBrowser( if err := b.connect(); err != nil { return nil, err } + + // cache the browser version information. + var err error + if b.version, err = b.fetchVersion(); err != nil { + return nil, err + } + return b, nil } @@ -172,7 +191,7 @@ func (b *Browser) getPages() []*Page { return pages } -func (b *Browser) initEvents() error { +func (b *Browser) initEvents() error { //nolint:cyclop var cancelCtx context.Context cancelCtx, b.evCancelFn = context.WithCancel(b.ctx) chHandler := make(chan Event) @@ -198,7 +217,9 @@ func (b *Browser) initEvents() error { case event := <-chHandler: if ev, ok := event.data.(*target.EventAttachedToTarget); ok { b.logger.Debugf("Browser:initEvents:onAttachedToTarget", "sid:%v tid:%v", ev.SessionID, ev.TargetInfo.TargetID) - b.onAttachedToTarget(ev) + if err := b.onAttachedToTarget(ev); err != nil { + k6ext.Panic(b.ctx, "browser is attaching to target: %w", err) + } } else if ev, ok := event.data.(*target.EventDetachedFromTarget); ok { b.logger.Debugf("Browser:initEvents:onDetachedFromTarget", "sid:%v", ev.SessionID) b.onDetachedFromTarget(ev) @@ -247,7 +268,7 @@ func (b *Browser) connectionOnAttachedToTarget(eva *target.EventAttachedToTarget } // onAttachedToTarget is called when a new page is attached to the browser. -func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) { +func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) error { b.logger.Debugf("Browser:onAttachedToTarget", "sid:%v tid:%v bctxid:%v", ev.SessionID, ev.TargetInfo.TargetID, ev.TargetInfo.BrowserContextID) @@ -257,14 +278,14 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) { ) if !b.isAttachedPageValid(ev, browserCtx) { - return // Ignore this page. + return nil // Ignore this page. } session := b.conn.getSession(ev.SessionID) if session == nil { b.logger.Debugf("Browser:onAttachedToTarget", "session closed before attachToTarget is handled. sid:%v tid:%v", ev.SessionID, targetPage.TargetID) - return // ignore + return nil // ignore } var ( @@ -281,17 +302,21 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) { } p, err := NewPage(b.ctx, session, browserCtx, targetPage.TargetID, opener, isPage, b.logger) if err != nil && b.isPageAttachmentErrorIgnorable(ev, session, err) { - return // Ignore this page. + return nil // Ignore this page. } if err != nil { - k6ext.Panic(b.ctx, "creating a new %s: %w", targetPage.Type, err) + return fmt.Errorf("creating a new %s: %w", targetPage.Type, err) } + b.attachNewPage(p, ev) // Register the page as an active page. + // Emit the page event only for pages, not for background pages. // Background pages are created by extensions. if isPage { browserCtx.emit(EventBrowserContextPage, p) } + + return nil } // attachNewPage registers the page as an active page and attaches the sessionID with the targetID. @@ -529,8 +554,7 @@ func (b *Browser) CloseContext() error { if b.context == nil { return errors.New("cannot close context as none is active in browser") } - b.context.Close() - return nil + return b.context.Close() } // Context returns the current browser context or nil. @@ -545,7 +569,7 @@ func (b *Browser) IsConnected() bool { } // NewContext creates a new incognito-like browser context. -func (b *Browser) NewContext(opts goja.Value) (*BrowserContext, error) { +func (b *Browser) NewContext(opts sobek.Value) (*BrowserContext, error) { _, span := TraceAPICall(b.ctx, "", "browser.newContext") defer span.End() @@ -586,7 +610,7 @@ func (b *Browser) NewContext(opts goja.Value) (*BrowserContext, error) { } // NewPage creates a new tab in the browser window. -func (b *Browser) NewPage(opts goja.Value) (*Page, error) { +func (b *Browser) NewPage(opts sobek.Value) (*Page, error) { _, span := TraceAPICall(b.ctx, "", "browser.newPage") defer span.End() @@ -623,21 +647,12 @@ func (b *Browser) On(event string) (bool, error) { // UserAgent returns the controlled browser's user agent string. func (b *Browser) UserAgent() string { - action := cdpbrowser.GetVersion() - _, _, _, ua, _, err := action.Do(cdp.WithExecutor(b.ctx, b.conn)) - if err != nil { - k6ext.Panic(b.ctx, "getting browser user agent: %w", err) - } - return ua + return b.version.userAgent } // Version returns the controlled browser's version. func (b *Browser) Version() string { - action := cdpbrowser.GetVersion() - _, product, _, _, _, err := action.Do(cdp.WithExecutor(b.ctx, b.conn)) - if err != nil { - k6ext.Panic(b.ctx, "getting browser version: %w", err) - } + product := b.version.product i := strings.Index(product, "/") if i == -1 { return product @@ -645,6 +660,22 @@ func (b *Browser) Version() string { return product[i+1:] } +// fetchVersion returns the browser version information. +func (b *Browser) fetchVersion() (browserVersion, error) { + var ( + bv browserVersion + err error + ) + bv.protocolVersion, bv.product, bv.revision, bv.userAgent, bv.jsVersion, err = cdpbrowser. + GetVersion(). + Do(cdp.WithExecutor(b.ctx, b.conn)) + if err != nil { + return browserVersion{}, fmt.Errorf("getting browser version information: %w", err) + } + + return bv, nil +} + // WsURL returns the Websocket URL that the browser is listening on for CDP clients. func (b *Browser) WsURL() string { return b.browserProc.WsURL() diff --git a/vendor/github.com/grafana/xk6-browser/common/browser_context.go b/vendor/github.com/grafana/xk6-browser/common/browser_context.go index 09db5b44de0..30a10a89aea 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser_context.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser_context.go @@ -7,19 +7,19 @@ import ( "strings" "time" + cdpbrowser "github.com/chromedp/cdproto/browser" + "github.com/chromedp/cdproto/cdp" + "github.com/chromedp/cdproto/network" + "github.com/chromedp/cdproto/storage" + "github.com/chromedp/cdproto/target" + "github.com/grafana/sobek" + "github.com/grafana/xk6-browser/common/js" "github.com/grafana/xk6-browser/k6error" "github.com/grafana/xk6-browser/k6ext" "github.com/grafana/xk6-browser/log" k6modules "go.k6.io/k6/js/modules" - - cdpbrowser "github.com/chromedp/cdproto/browser" - "github.com/chromedp/cdproto/cdp" - "github.com/chromedp/cdproto/network" - "github.com/chromedp/cdproto/storage" - "github.com/chromedp/cdproto/target" - "github.com/dop251/goja" ) // waitForEventType represents the event types that can be used when working @@ -161,15 +161,16 @@ func (b *BrowserContext) ClearPermissions() error { } // Close shuts down the browser context. -func (b *BrowserContext) Close() { +func (b *BrowserContext) Close() error { b.logger.Debugf("BrowserContext:Close", "bctxid:%v", b.id) if b.id == "" { - k6ext.Panic(b.ctx, "default browser context can't be closed") + return fmt.Errorf("default browser context can't be closed") } if err := b.browser.disposeContext(b.id); err != nil { - k6ext.Panic(b.ctx, "disposing browser context: %w", err) + return fmt.Errorf("disposing browser context: %w", err) } + return nil } // GrantPermissions enables the specified permissions, all others will be disabled. @@ -259,20 +260,22 @@ func (b *BrowserContext) SetDefaultTimeout(timeout int64) { } // SetGeolocation overrides the geo location of the user. -func (b *BrowserContext) SetGeolocation(geolocation goja.Value) { +func (b *BrowserContext) SetGeolocation(geolocation sobek.Value) error { b.logger.Debugf("BrowserContext:SetGeolocation", "bctxid:%v", b.id) g := NewGeolocation() if err := g.Parse(b.ctx, geolocation); err != nil { - k6ext.Panic(b.ctx, "parsing geo location: %v", err) + return fmt.Errorf("parsing geo location: %w", err) } b.opts.Geolocation = g for _, p := range b.browser.getPages() { if err := p.updateGeolocation(); err != nil { - k6ext.Panic(b.ctx, "updating geo location in target ID %s: %w", p.targetID, err) + return fmt.Errorf("updating geo location in target ID %s: %w", p.targetID, err) } } + + return nil } // SetHTTPCredentials sets username/password credentials to use for HTTP authentication. @@ -281,30 +284,41 @@ func (b *BrowserContext) SetGeolocation(geolocation goja.Value) { // See for details: // - https://github.com/microsoft/playwright/issues/2196#issuecomment-627134837 // - https://github.com/microsoft/playwright/pull/2763 -func (b *BrowserContext) SetHTTPCredentials(httpCredentials goja.Value) { +func (b *BrowserContext) SetHTTPCredentials(httpCredentials sobek.Value) error { b.logger.Warnf("setHTTPCredentials", "setHTTPCredentials is deprecated."+ " Create a new BrowserContext with httpCredentials instead.") b.logger.Debugf("BrowserContext:SetHTTPCredentials", "bctxid:%v", b.id) c := NewCredentials() if err := c.Parse(b.ctx, httpCredentials); err != nil { - k6ext.Panic(b.ctx, "setting HTTP credentials: %w", err) + return fmt.Errorf("parsing HTTP credentials: %w", err) } b.opts.HttpCredentials = c for _, p := range b.browser.getPages() { - p.updateHttpCredentials() + if err := p.updateHTTPCredentials(); err != nil { + return fmt.Errorf("setting HTTP credentials in target ID %s: %w", p.targetID, err) + } } + + return nil } // SetOffline toggles the browser's connectivity on/off. -func (b *BrowserContext) SetOffline(offline bool) { +func (b *BrowserContext) SetOffline(offline bool) error { b.logger.Debugf("BrowserContext:SetOffline", "bctxid:%v offline:%t", b.id, offline) b.opts.Offline = offline for _, p := range b.browser.getPages() { - p.updateOffline() + if err := p.updateOffline(); err != nil { + return fmt.Errorf( + "setting offline status to %t for the browser context ID %s: %w", + offline, b.id, err, + ) + } } + + return nil } // Timeout will return the default timeout or the one set by the user. diff --git a/vendor/github.com/grafana/xk6-browser/common/browser_context_options.go b/vendor/github.com/grafana/xk6-browser/common/browser_context_options.go index a47b02942c7..507cde33d85 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser_context_options.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser_context_options.go @@ -6,9 +6,9 @@ import ( "fmt" "time" - "github.com/grafana/xk6-browser/k6ext" + "github.com/grafana/sobek" - "github.com/dop251/goja" + "github.com/grafana/xk6-browser/k6ext" ) // Geolocation represents a geolocation. @@ -24,13 +24,13 @@ func NewGeolocation() *Geolocation { } // Parse parses the geolocation options. -func (g *Geolocation) Parse(ctx context.Context, opts goja.Value) error { //nolint:cyclop +func (g *Geolocation) Parse(ctx context.Context, opts sobek.Value) error { //nolint:cyclop rt := k6ext.Runtime(ctx) longitude := 0.0 latitude := 0.0 accuracy := 0.0 - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -100,95 +100,98 @@ func NewBrowserContextOptions() *BrowserContextOptions { } } -func (b *BrowserContextOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the browser context options. +func (b *BrowserContextOptions) Parse(ctx context.Context, opts sobek.Value) error { //nolint:cyclop,funlen,gocognit rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { - opts := opts.ToObject(rt) - for _, k := range opts.Keys() { - switch k { - case "acceptDownloads": - b.AcceptDownloads = opts.Get(k).ToBoolean() - case "bypassCSP": - b.BypassCSP = opts.Get(k).ToBoolean() - case "colorScheme": - switch ColorScheme(opts.Get(k).String()) { - case "light": - b.ColorScheme = ColorSchemeLight - case "dark": - b.ColorScheme = ColorSchemeDark - default: - b.ColorScheme = ColorSchemeNoPreference - } - case "deviceScaleFactor": - b.DeviceScaleFactor = opts.Get(k).ToFloat() - case "extraHTTPHeaders": - headers := opts.Get(k).ToObject(rt) - for _, k := range headers.Keys() { - b.ExtraHTTPHeaders[k] = headers.Get(k).String() - } - case "geolocation": - geolocation := NewGeolocation() - if err := geolocation.Parse(ctx, opts.Get(k).ToObject(rt)); err != nil { - return err - } - b.Geolocation = geolocation - case "hasTouch": - b.HasTouch = opts.Get(k).ToBoolean() - case "httpCredentials": - credentials := NewCredentials() - if err := credentials.Parse(ctx, opts.Get(k).ToObject(rt)); err != nil { - return err - } - b.HttpCredentials = credentials - case "ignoreHTTPSErrors": - b.IgnoreHTTPSErrors = opts.Get(k).ToBoolean() - case "isMobile": - b.IsMobile = opts.Get(k).ToBoolean() - case "javaScriptEnabled": - b.JavaScriptEnabled = opts.Get(k).ToBoolean() - case "locale": - b.Locale = opts.Get(k).String() - case "offline": - b.Offline = opts.Get(k).ToBoolean() - case "permissions": - if ps, ok := opts.Get(k).Export().([]any); ok { - for _, p := range ps { - b.Permissions = append(b.Permissions, fmt.Sprintf("%v", p)) - } - } - case "reducedMotion": - switch ReducedMotion(opts.Get(k).String()) { - case "reduce": - b.ReducedMotion = ReducedMotionReduce - default: - b.ReducedMotion = ReducedMotionNoPreference - } - case "screen": - screen := &Screen{} - if err := screen.Parse(ctx, opts.Get(k).ToObject(rt)); err != nil { - return err - } - b.Screen = screen - case "timezoneID": - b.TimezoneID = opts.Get(k).String() - case "userAgent": - b.UserAgent = opts.Get(k).String() - case "viewport": - viewport := &Viewport{} - if err := viewport.Parse(ctx, opts.Get(k).ToObject(rt)); err != nil { - return err + if !sobekValueExists(opts) { + return nil + } + o := opts.ToObject(rt) + for _, k := range o.Keys() { + switch k { + case "acceptDownloads": + b.AcceptDownloads = o.Get(k).ToBoolean() + case "bypassCSP": + b.BypassCSP = o.Get(k).ToBoolean() + case "colorScheme": + switch ColorScheme(o.Get(k).String()) { //nolint:exhaustive + case "light": + b.ColorScheme = ColorSchemeLight + case "dark": + b.ColorScheme = ColorSchemeDark + default: + b.ColorScheme = ColorSchemeNoPreference + } + case "deviceScaleFactor": + b.DeviceScaleFactor = o.Get(k).ToFloat() + case "extraHTTPHeaders": + headers := o.Get(k).ToObject(rt) + for _, k := range headers.Keys() { + b.ExtraHTTPHeaders[k] = headers.Get(k).String() + } + case "geolocation": + geolocation := NewGeolocation() + if err := geolocation.Parse(ctx, o.Get(k).ToObject(rt)); err != nil { + return err + } + b.Geolocation = geolocation + case "hasTouch": + b.HasTouch = o.Get(k).ToBoolean() + case "httpCredentials": + credentials := NewCredentials() + if err := credentials.Parse(ctx, o.Get(k).ToObject(rt)); err != nil { + return err + } + b.HttpCredentials = credentials + case "ignoreHTTPSErrors": + b.IgnoreHTTPSErrors = o.Get(k).ToBoolean() + case "isMobile": + b.IsMobile = o.Get(k).ToBoolean() + case "javaScriptEnabled": + b.JavaScriptEnabled = o.Get(k).ToBoolean() + case "locale": + b.Locale = o.Get(k).String() + case "offline": + b.Offline = o.Get(k).ToBoolean() + case "permissions": + if ps, ok := o.Get(k).Export().([]any); ok { + for _, p := range ps { + b.Permissions = append(b.Permissions, fmt.Sprintf("%v", p)) } - b.Viewport = viewport } + case "reducedMotion": + switch ReducedMotion(o.Get(k).String()) { //nolint:exhaustive + case "reduce": + b.ReducedMotion = ReducedMotionReduce + default: + b.ReducedMotion = ReducedMotionNoPreference + } + case "screen": + screen := &Screen{} + if err := screen.Parse(ctx, o.Get(k).ToObject(rt)); err != nil { + return err + } + b.Screen = screen + case "timezoneID": + b.TimezoneID = o.Get(k).String() + case "userAgent": + b.UserAgent = o.Get(k).String() + case "viewport": + viewport := &Viewport{} + if err := viewport.Parse(ctx, o.Get(k).ToObject(rt)); err != nil { + return err + } + b.Viewport = viewport } } + return nil } // WaitForEventOptions are the options used by the browserContext.waitForEvent API. type WaitForEventOptions struct { Timeout time.Duration - PredicateFn goja.Callable + PredicateFn sobek.Callable } // NewWaitForEventOptions created a new instance of WaitForEventOptions with a @@ -202,8 +205,8 @@ func NewWaitForEventOptions(defaultTimeout time.Duration) *WaitForEventOptions { // Parse will parse the options or a callable predicate function. It can parse // only a callable predicate function or an object which contains a callable // predicate function and a timeout. -func (w *WaitForEventOptions) Parse(ctx context.Context, optsOrPredicate goja.Value) error { - if !gojaValueExists(optsOrPredicate) { +func (w *WaitForEventOptions) Parse(ctx context.Context, optsOrPredicate sobek.Value) error { + if !sobekValueExists(optsOrPredicate) { return nil } @@ -212,7 +215,7 @@ func (w *WaitForEventOptions) Parse(ctx context.Context, optsOrPredicate goja.Va rt = k6ext.Runtime(ctx) ) - w.PredicateFn, isCallable = goja.AssertFunction(optsOrPredicate) + w.PredicateFn, isCallable = sobek.AssertFunction(optsOrPredicate) if isCallable { return nil } @@ -221,7 +224,7 @@ func (w *WaitForEventOptions) Parse(ctx context.Context, optsOrPredicate goja.Va for _, k := range opts.Keys() { switch k { case "predicate": - w.PredicateFn, isCallable = goja.AssertFunction(opts.Get(k)) + w.PredicateFn, isCallable = sobek.AssertFunction(opts.Get(k)) if !isCallable { return errors.New("predicate function is not callable") } @@ -243,11 +246,11 @@ func NewGrantPermissionsOptions() *GrantPermissionsOptions { return &GrantPermissionsOptions{} } -// Parse parses the options from opts if opts exists in the Goja runtime. -func (g *GrantPermissionsOptions) Parse(ctx context.Context, opts goja.Value) { +// Parse parses the options from opts if opts exists in the sobek runtime. +func (g *GrantPermissionsOptions) Parse(ctx context.Context, opts sobek.Value) { rt := k6ext.Runtime(ctx) - if gojaValueExists(opts) { + if sobekValueExists(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { if k == "origin" { diff --git a/vendor/github.com/grafana/xk6-browser/common/connection.go b/vendor/github.com/grafana/xk6-browser/common/connection.go index d8e1923c477..40645ec36ea 100644 --- a/vendor/github.com/grafana/xk6-browser/common/connection.go +++ b/vendor/github.com/grafana/xk6-browser/common/connection.go @@ -16,8 +16,8 @@ import ( "github.com/chromedp/cdproto/cdp" cdpruntime "github.com/chromedp/cdproto/runtime" "github.com/chromedp/cdproto/target" - "github.com/dop251/goja" "github.com/gorilla/websocket" + "github.com/grafana/sobek" "github.com/mailru/easyjson" "github.com/mailru/easyjson/jlexer" "github.com/mailru/easyjson/jwriter" @@ -51,7 +51,7 @@ type executorEmitter interface { type connection interface { executorEmitter - Close(...goja.Value) + Close(...sobek.Value) IgnoreIOErrors() getSession(target.SessionID) *Session } @@ -559,7 +559,7 @@ func (c *Connection) sendLoop() { // Close cleanly closes the WebSocket connection. // It returns an error if sending the Close control frame fails. -func (c *Connection) Close(args ...goja.Value) { +func (c *Connection) Close(args ...sobek.Value) { code := websocket.CloseGoingAway if len(args) > 0 { code = int(args[0].ToInteger()) diff --git a/vendor/github.com/grafana/xk6-browser/common/element_handle.go b/vendor/github.com/grafana/xk6-browser/common/element_handle.go index 5eebcb6d8d2..5c9d25f54af 100644 --- a/vendor/github.com/grafana/xk6-browser/common/element_handle.go +++ b/vendor/github.com/grafana/xk6-browser/common/element_handle.go @@ -12,7 +12,7 @@ import ( "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/dom" cdppage "github.com/chromedp/cdproto/page" - "github.com/dop251/goja" + "github.com/grafana/sobek" "go.opentelemetry.io/otel/attribute" "github.com/grafana/xk6-browser/common/js" @@ -64,7 +64,10 @@ func (h *ElementHandle) boundingBox() (*Rect, error) { } func (h *ElementHandle) checkHitTargetAt(apiCtx context.Context, point Position) (bool, error) { - frame := h.ownerFrame(apiCtx) + frame, err := h.ownerFrame(apiCtx) + if err != nil { + return false, fmt.Errorf("checking hit target at %v: %w", point, err) + } if frame != nil && frame.parentFrame != nil { el, err := h.frame.FrameElement() if err != nil { @@ -228,7 +231,7 @@ func compensateHalfIntegerRoundingError(p Position) Position { return p } -func (h *ElementHandle) dblClick(p *Position, opts *MouseClickOptions) error { +func (h *ElementHandle) dblclick(p *Position, opts *MouseClickOptions) error { return h.frame.page.Mouse.click(p.X, p.Y, opts) } @@ -271,7 +274,9 @@ func (h *ElementHandle) fill(_ context.Context, value string) error { } if s == resultNeedsInput { - h.frame.page.Keyboard.InsertText(value) + if err := h.frame.page.Keyboard.InsertText(value); err != nil { + return fmt.Errorf("fill: %w", err) + } } else if s != resultDone { // Either we're done or an error happened (returned as "error:..." from JS) return errorFromDOMError(s) @@ -422,22 +427,26 @@ func (h *ElementHandle) offsetPosition(apiCtx context.Context, offset *Position) }, nil } -func (h *ElementHandle) ownerFrame(apiCtx context.Context) *Frame { - frameId := h.frame.page.getOwnerFrame(apiCtx, h) - if frameId == "" { - return nil +func (h *ElementHandle) ownerFrame(apiCtx context.Context) (*Frame, error) { + frameID, err := h.frame.page.getOwnerFrame(apiCtx, h) + if err != nil { + return nil, err + } + if frameID == "" { + return nil, nil //nolint:nilnil } - frame, ok := h.frame.page.frameManager.getFrameByID(frameId) + frame, ok := h.frame.page.frameManager.getFrameByID(frameID) if ok { - return frame + return frame, nil } for _, page := range h.frame.page.browserCtx.browser.pages { - frame, ok = page.frameManager.getFrameByID(frameId) + frame, ok = page.frameManager.getFrameByID(frameID) if ok { - return frame + return frame, nil } } - return nil + + return nil, nil //nolint:nilnil } func (h *ElementHandle) scrollRectIntoViewIfNeeded(apiCtx context.Context, rect *dom.Rect) error { @@ -468,9 +477,9 @@ func (h *ElementHandle) press(apiCtx context.Context, key string, opts *Keyboard } //nolint:funlen,gocognit,cyclop -func (h *ElementHandle) selectOption(apiCtx context.Context, values goja.Value) (any, error) { - convertSelectOptionValues := func(values goja.Value) ([]any, error) { - if goja.IsNull(values) || goja.IsUndefined(values) { +func (h *ElementHandle) selectOption(apiCtx context.Context, values sobek.Value) (any, error) { + convertSelectOptionValues := func(values sobek.Value) ([]any, error) { + if sobek.IsNull(values) || sobek.IsUndefined(values) { return nil, nil } @@ -489,7 +498,7 @@ func (h *ElementHandle) selectOption(apiCtx context.Context, values goja.Value) return nil, fmt.Errorf("options[%d]: expected object, got null", i) case reflect.TypeOf(&ElementHandle{}).Kind(): opts = append(opts, t.(*ElementHandle)) - case reflect.TypeOf(goja.Object{}).Kind(): + case reflect.TypeOf(sobek.Object{}).Kind(): obj := values.ToObject(rt) opt := SelectOption{} for _, k := range obj.Keys() { @@ -514,7 +523,7 @@ func (h *ElementHandle) selectOption(apiCtx context.Context, values goja.Value) } case reflect.TypeOf(&ElementHandle{}).Kind(): opts = append(opts, t.(*ElementHandle)) - case reflect.TypeOf(goja.Object{}).Kind(): + case reflect.TypeOf(sobek.Object{}).Kind(): obj := values.ToObject(rt) opt := SelectOption{} for _, k := range obj.Keys() { @@ -614,8 +623,10 @@ func (h *ElementHandle) typ(apiCtx context.Context, text string, opts *KeyboardO return nil } -func (h *ElementHandle) waitAndScrollIntoViewIfNeeded(apiCtx context.Context, force, noWaitAfter bool, timeout time.Duration) error { - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { +func (h *ElementHandle) waitAndScrollIntoViewIfNeeded( + _ context.Context, force, noWaitAfter bool, timeout time.Duration, +) error { + fn := func(apiCtx context.Context, _ *ElementHandle) (any, error) { fn := ` (element) => { element.scrollIntoViewIfNeeded(true); @@ -626,6 +637,7 @@ func (h *ElementHandle) waitAndScrollIntoViewIfNeeded(apiCtx context.Context, fo forceCallable: true, returnByValue: true, } + return h.eval(apiCtx, opts, fn) } actFn := h.newAction([]string{"visible", "stable"}, fn, force, noWaitAfter, timeout) @@ -748,31 +760,36 @@ func (h *ElementHandle) ContentFrame() (*Frame, error) { return frame, nil } -func (h *ElementHandle) Dblclick(opts goja.Value) { - actionOpts := NewElementHandleDblclickOptions(h.defaultTimeout()) - if err := actionOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing element double click options: %w", err) +// Dblclick scrolls element into view and double clicks on the element. +func (h *ElementHandle) Dblclick(opts sobek.Value) error { + popts := NewElementHandleDblclickOptions(h.defaultTimeout()) + if err := popts.Parse(h.ctx, opts); err != nil { + return fmt.Errorf("parsing element double click options: %w", err) } - fn := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) { - return nil, handle.dblClick(p, actionOpts.ToMouseClickOptions()) + + dblclick := func(_ context.Context, handle *ElementHandle, p *Position) (any, error) { + return nil, handle.dblclick(p, popts.ToMouseClickOptions()) } - pointerFn := h.newPointerAction(fn, &actionOpts.ElementHandleBasePointerOptions) - _, err := call(h.ctx, pointerFn, actionOpts.Timeout) - if err != nil { - k6ext.Panic(h.ctx, "double clicking on element: %w", err) + dblclickAction := h.newPointerAction(dblclick, &popts.ElementHandleBasePointerOptions) + if _, err := call(h.ctx, dblclickAction, popts.Timeout); err != nil { + return fmt.Errorf("double clicking on element: %w", err) } + applySlowMo(h.ctx) + + return nil } // DispatchEvent dispatches a DOM event to the element. func (h *ElementHandle) DispatchEvent(typ string, eventInit any) error { - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { + dispatchEvent := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.dispatchEvent(apiCtx, typ, eventInit) } opts := NewElementHandleBaseOptions(h.defaultTimeout()) - actFn := h.newAction([]string{}, fn, opts.Force, opts.NoWaitAfter, opts.Timeout) - _, err := call(h.ctx, actFn, opts.Timeout) - if err != nil { + dispatchEventAction := h.newAction( + []string{}, dispatchEvent, opts.Force, opts.NoWaitAfter, opts.Timeout, + ) + if _, err := call(h.ctx, dispatchEventAction, opts.Timeout); err != nil { return fmt.Errorf("dispatching element event %q: %w", typ, err) } @@ -781,184 +798,236 @@ func (h *ElementHandle) DispatchEvent(typ string, eventInit any) error { return nil } -func (h *ElementHandle) Fill(value string, opts goja.Value) { - actionOpts := NewElementHandleBaseOptions(h.defaultTimeout()) - if err := actionOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing element fill options: %w", err) +// Fill types the given value into the element. +func (h *ElementHandle) Fill(value string, opts sobek.Value) error { + popts := NewElementHandleBaseOptions(h.defaultTimeout()) + if err := popts.Parse(h.ctx, opts); err != nil { + return fmt.Errorf("parsing element fill options: %w", err) } - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { + + fill := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return nil, handle.fill(apiCtx, value) } - actFn := h.newAction([]string{"visible", "enabled", "editable"}, - fn, actionOpts.Force, actionOpts.NoWaitAfter, actionOpts.Timeout) - _, err := call(h.ctx, actFn, actionOpts.Timeout) - if err != nil { - k6ext.Panic(h.ctx, "handling element fill action: %w", err) + fillAction := h.newAction( + []string{"visible", "enabled", "editable"}, + fill, popts.Force, popts.NoWaitAfter, popts.Timeout, + ) + if _, err := call(h.ctx, fillAction, popts.Timeout); err != nil { + return fmt.Errorf("filling element: %w", err) } + applySlowMo(h.ctx) + + return nil } // Focus scrolls element into view and focuses the element. -func (h *ElementHandle) Focus() { - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { +func (h *ElementHandle) Focus() error { + focus := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return nil, handle.focus(apiCtx, false) } opts := NewElementHandleBaseOptions(h.defaultTimeout()) - actFn := h.newAction([]string{}, fn, opts.Force, opts.NoWaitAfter, opts.Timeout) - _, err := call(h.ctx, actFn, opts.Timeout) - if err != nil { - k6ext.Panic(h.ctx, "focusing on element: %w", err) + focusAction := h.newAction( + []string{}, focus, opts.Force, opts.NoWaitAfter, opts.Timeout, + ) + if _, err := call(h.ctx, focusAction, opts.Timeout); err != nil { + return fmt.Errorf("focusing on element: %w", err) } + applySlowMo(h.ctx) + + return nil } // GetAttribute retrieves the value of specified element attribute. -func (h *ElementHandle) GetAttribute(name string) any { - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { +// The second return value is true if the attribute exists, and false otherwise. +func (h *ElementHandle) GetAttribute(name string) (string, bool, error) { + getAttribute := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.getAttribute(apiCtx, name) } opts := NewElementHandleBaseOptions(h.defaultTimeout()) - actFn := h.newAction([]string{}, fn, opts.Force, opts.NoWaitAfter, opts.Timeout) - v, err := call(h.ctx, actFn, opts.Timeout) + getAttributeAction := h.newAction( + []string{}, getAttribute, opts.Force, opts.NoWaitAfter, opts.Timeout, + ) + + v, err := call(h.ctx, getAttributeAction, opts.Timeout) if err != nil { - k6ext.Panic(h.ctx, "getting attribute of %q: %q", name, err) + return "", false, fmt.Errorf("getting attribute %q of element: %w", name, err) + } + if v == nil { + return "", false, nil + } + s, ok := v.(string) + if !ok { + return "", false, fmt.Errorf( + "getting attribute %q of element: unexpected type %T (expecting string)", + name, v, + ) } - applySlowMo(h.ctx) - - // TODO: return any with error - return v + return s, true, nil } // Hover scrolls element into view and hovers over its center point. -func (h *ElementHandle) Hover(opts goja.Value) { - actionOpts := NewElementHandleHoverOptions(h.defaultTimeout()) - if err := actionOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing element hover options: %w", err) +func (h *ElementHandle) Hover(opts sobek.Value) error { + aopts := NewElementHandleHoverOptions(h.defaultTimeout()) + if err := aopts.Parse(h.ctx, opts); err != nil { + return fmt.Errorf("parsing element hover options: %w", err) } - fn := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) { + + hover := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) { return nil, handle.hover(apiCtx, p) } - pointerFn := h.newPointerAction(fn, &actionOpts.ElementHandleBasePointerOptions) - _, err := call(h.ctx, pointerFn, actionOpts.Timeout) - if err != nil { - k6ext.Panic(h.ctx, "hovering on element: %w", err) + hoverAction := h.newPointerAction(hover, &aopts.ElementHandleBasePointerOptions) + if _, err := call(h.ctx, hoverAction, aopts.Timeout); err != nil { + return fmt.Errorf("hovering on element: %w", err) } + applySlowMo(h.ctx) + + return nil } // InnerHTML returns the inner HTML of the element. -func (h *ElementHandle) InnerHTML() string { - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { +func (h *ElementHandle) InnerHTML() (string, error) { + innerHTML := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.innerHTML(apiCtx) } opts := NewElementHandleBaseOptions(h.defaultTimeout()) - actFn := h.newAction([]string{}, fn, opts.Force, opts.NoWaitAfter, opts.Timeout) - v, err := call(h.ctx, actFn, opts.Timeout) + innerHTMLAction := h.newAction( + []string{}, innerHTML, opts.Force, opts.NoWaitAfter, opts.Timeout, + ) + v, err := call(h.ctx, innerHTMLAction, opts.Timeout) if err != nil { - k6ext.Panic(h.ctx, "getting element's inner HTML: %w", err) + return "", fmt.Errorf("getting element's inner HTML: %w", err) } + applySlowMo(h.ctx) - // TODO: handle error + s, ok := v.(string) + if !ok { + return "", fmt.Errorf("unexpected type %T (expecting string)", v) + } - return v.(string) //nolint:forcetypeassert + return s, nil } // InnerText returns the inner text of the element. -func (h *ElementHandle) InnerText() string { - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { +func (h *ElementHandle) InnerText() (string, error) { + innerText := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.innerText(apiCtx) } opts := NewElementHandleBaseOptions(h.defaultTimeout()) - actFn := h.newAction([]string{}, fn, opts.Force, opts.NoWaitAfter, opts.Timeout) - v, err := call(h.ctx, actFn, opts.Timeout) + innerTextAction := h.newAction( + []string{}, innerText, opts.Force, opts.NoWaitAfter, opts.Timeout.Abs(), + ) + v, err := call(h.ctx, innerTextAction, opts.Timeout) if err != nil { - k6ext.Panic(h.ctx, "getting element's inner text: %w", err) + return "", fmt.Errorf("getting element's inner text: %w", err) } + applySlowMo(h.ctx) - // TODO: handle error + s, ok := v.(string) + if !ok { + return "", fmt.Errorf("unexpected type %T (expecting string)", v) + } - return v.(string) //nolint:forcetypeassert + return s, nil } -func (h *ElementHandle) InputValue(opts goja.Value) string { - actionOpts := NewElementHandleBaseOptions(h.defaultTimeout()) - if err := actionOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing element input value options: %w", err) +// InputValue returns the value of the input element. +func (h *ElementHandle) InputValue(opts sobek.Value) (string, error) { + aopts := NewElementHandleBaseOptions(h.defaultTimeout()) + if err := aopts.Parse(h.ctx, opts); err != nil { + return "", fmt.Errorf("parsing element input value options: %w", err) } - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { + + inputValue := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.inputValue(apiCtx) } - actFn := h.newAction([]string{}, fn, actionOpts.Force, actionOpts.NoWaitAfter, actionOpts.Timeout) - v, err := call(h.ctx, actFn, actionOpts.Timeout) + inputValueAction := h.newAction([]string{}, inputValue, aopts.Force, aopts.NoWaitAfter, aopts.Timeout) + v, err := call(h.ctx, inputValueAction, aopts.Timeout) if err != nil { - k6ext.Panic(h.ctx, "getting element's input value: %w", err) + return "", fmt.Errorf("getting element's input value: %w", err) } - applySlowMo(h.ctx) - // TODO: return error + s, ok := v.(string) + if !ok { + return "", fmt.Errorf("unexpected type %T (expecting string)", v) + } - return v.(string) //nolint:forcetypeassert + return s, nil } // IsChecked checks if a checkbox or radio is checked. -func (h *ElementHandle) IsChecked() bool { - result, err := h.isChecked(h.ctx, 0) - if err != nil && !errors.Is(err, ErrTimedOut) { // We don't care anout timeout errors here! - k6ext.Panic(h.ctx, "checking element is checked: %w", err) +func (h *ElementHandle) IsChecked() (bool, error) { + ok, err := h.isChecked(h.ctx, 0) + // We don't care about timeout errors here! + if err != nil && !errors.Is(err, ErrTimedOut) { + return false, fmt.Errorf("checking element is checked: %w", err) } - return result + + return ok, nil } // IsDisabled checks if the element is disabled. -func (h *ElementHandle) IsDisabled() bool { - result, err := h.isDisabled(h.ctx, 0) - if err != nil && !errors.Is(err, ErrTimedOut) { // We don't care anout timeout errors here! - k6ext.Panic(h.ctx, "checking element is disabled: %w", err) +func (h *ElementHandle) IsDisabled() (bool, error) { + ok, err := h.isDisabled(h.ctx, 0) + // We don't care anout timeout errors here! + if err != nil && !errors.Is(err, ErrTimedOut) { + return false, fmt.Errorf("checking element is disabled: %w", err) } - return result + + return ok, nil } // IsEditable checks if the element is editable. -func (h *ElementHandle) IsEditable() bool { - result, err := h.isEditable(h.ctx, 0) - if err != nil && !errors.Is(err, ErrTimedOut) { // We don't care anout timeout errors here! - k6ext.Panic(h.ctx, "checking element is editable: %w", err) +func (h *ElementHandle) IsEditable() (bool, error) { + ok, err := h.isEditable(h.ctx, 0) + // We don't care anout timeout errors here! + if err != nil && !errors.Is(err, ErrTimedOut) { + return false, fmt.Errorf("checking element is editable: %w", err) } - return result + + return ok, nil } // IsEnabled checks if the element is enabled. -func (h *ElementHandle) IsEnabled() bool { - result, err := h.isEnabled(h.ctx, 0) - if err != nil && !errors.Is(err, ErrTimedOut) { // We don't care anout timeout errors here! - k6ext.Panic(h.ctx, "checking element is enabled: %w", err) +func (h *ElementHandle) IsEnabled() (bool, error) { + ok, err := h.isEnabled(h.ctx, 0) + // We don't care anout timeout errors here! + if err != nil && !errors.Is(err, ErrTimedOut) { + return false, fmt.Errorf("checking element is enabled: %w", err) } - return result + + return ok, nil } // IsHidden checks if the element is hidden. -func (h *ElementHandle) IsHidden() bool { - result, err := h.isHidden(h.ctx) - if err != nil && !errors.Is(err, ErrTimedOut) { // We don't care anout timeout errors here! - k6ext.Panic(h.ctx, "checking element is hidden: %w", err) +func (h *ElementHandle) IsHidden() (bool, error) { + ok, err := h.isHidden(h.ctx) + // We don't care anout timeout errors here! + if err != nil && !errors.Is(err, ErrTimedOut) { + return false, fmt.Errorf("checking element is hidden: %w", err) } - return result + + return ok, nil } // IsVisible checks if the element is visible. -func (h *ElementHandle) IsVisible() bool { - result, err := h.isVisible(h.ctx) - if err != nil && !errors.Is(err, ErrTimedOut) { // We don't care anout timeout errors here! - k6ext.Panic(h.ctx, "checking element is visible: %w", err) +func (h *ElementHandle) IsVisible() (bool, error) { + ok, err := h.isVisible(h.ctx) + // We don't care anout timeout errors here! + if err != nil && !errors.Is(err, ErrTimedOut) { + return false, fmt.Errorf("checking element is visible: %w", err) } - return result + + return ok, nil } // OwnerFrame returns the frame containing this element. -func (h *ElementHandle) OwnerFrame() (*Frame, error) { +func (h *ElementHandle) OwnerFrame() (_ *Frame, rerr error) { fn := ` (node, injected) => { return injected.getDocumentElement(node); @@ -980,7 +1049,13 @@ func (h *ElementHandle) OwnerFrame() (*Frame, error) { if !ok { return nil, fmt.Errorf("unexpected result type while getting document element: %T", res) } - defer documentHandle.Dispose() + defer func() { + if err := documentHandle.Dispose(); err != nil { + err = fmt.Errorf("disposing document element: %w", err) + rerr = errors.Join(err, rerr) + } + }() + if documentHandle.remoteObject.ObjectID == "" { return nil, err } @@ -1002,30 +1077,36 @@ func (h *ElementHandle) OwnerFrame() (*Frame, error) { return frame, nil } -func (h *ElementHandle) Press(key string, opts goja.Value) { - parsedOpts := NewElementHandlePressOptions(h.defaultTimeout()) - if err := parsedOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing press %q options: %v", key, err) +// Press scrolls element into view and presses the given keys. +func (h *ElementHandle) Press(key string, opts sobek.Value) error { + popts := NewElementHandlePressOptions(h.defaultTimeout()) + if err := popts.Parse(h.ctx, opts); err != nil { + return fmt.Errorf("parsing press %q options: %w", key, err) } - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { + + press := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return nil, handle.press(apiCtx, key, NewKeyboardOptions()) } - actFn := h.newAction([]string{}, fn, false, parsedOpts.NoWaitAfter, parsedOpts.Timeout) - _, err := call(h.ctx, actFn, parsedOpts.Timeout) - if err != nil { - k6ext.Panic(h.ctx, "pressing %q: %v", key, err) + pressAction := h.newAction( + []string{}, press, false, popts.NoWaitAfter, popts.Timeout, + ) + if _, err := call(h.ctx, pressAction, popts.Timeout); err != nil { + return fmt.Errorf("pressing %q on element: %w", key, err) } + applySlowMo(h.ctx) + + return nil } // Query runs "element.querySelector" within the page. If no element matches the selector, // the return value resolves to "null". -func (h *ElementHandle) Query(selector string, strict bool) (*ElementHandle, error) { +func (h *ElementHandle) Query(selector string, strict bool) (_ *ElementHandle, rerr error) { parsedSelector, err := NewSelector(selector) if err != nil { - k6ext.Panic(h.ctx, "parsing selector %q: %w", selector, err) + return nil, fmt.Errorf("parsing selector %q: %w", selector, err) } - fn := ` + querySelector := ` (node, injected, selector, strict) => { return injected.querySelector(selector, strict, node || document); } @@ -1034,7 +1115,7 @@ func (h *ElementHandle) Query(selector string, strict bool) (*ElementHandle, err forceCallable: true, returnByValue: false, } - result, err := h.evalWithScript(h.ctx, opts, fn, parsedSelector, strict) + result, err := h.evalWithScript(h.ctx, opts, querySelector, parsedSelector, strict) if err != nil { return nil, fmt.Errorf("querying selector %q: %w", selector, err) } @@ -1047,7 +1128,12 @@ func (h *ElementHandle) Query(selector string, strict bool) (*ElementHandle, err } element := handle.AsElement() if element == nil { - handle.Dispose() + defer func() { + if err := handle.Dispose(); err != nil { + err = fmt.Errorf("disposing element handle: %w", err) + rerr = errors.Join(err, rerr) + } + }() return nil, fmt.Errorf("querying selector %q", selector) } @@ -1057,8 +1143,6 @@ func (h *ElementHandle) Query(selector string, strict bool) (*ElementHandle, err // QueryAll queries element subtree for matching elements. // If no element matches the selector, the return value resolves to "null". func (h *ElementHandle) QueryAll(selector string) ([]*ElementHandle, error) { - defer applySlowMo(h.ctx) - handles, err := h.queryAll(selector, h.evalWithScript) if err != nil { return nil, fmt.Errorf("querying all selector %q: %w", selector, err) @@ -1067,7 +1151,7 @@ func (h *ElementHandle) QueryAll(selector string) ([]*ElementHandle, error) { return handles, nil } -func (h *ElementHandle) queryAll(selector string, eval evalFunc) ([]*ElementHandle, error) { +func (h *ElementHandle) queryAll(selector string, eval evalFunc) (_ []*ElementHandle, rerr error) { parsedSelector, err := NewSelector(selector) if err != nil { return nil, fmt.Errorf("parsing selector %q: %w", selector, err) @@ -1090,7 +1174,12 @@ func (h *ElementHandle) queryAll(selector string, eval evalFunc) ([]*ElementHand if !ok { return nil, fmt.Errorf("getting element handle for selector %q: %w", selector, ErrJSHandleInvalid) } - defer handles.Dispose() + defer func() { + if err := handles.Dispose(); err != nil { + err = fmt.Errorf("disposing element handles: %w", err) + rerr = errors.Join(err, rerr) + } + }() props, err := handles.GetProperties() if err != nil { @@ -1102,8 +1191,8 @@ func (h *ElementHandle) queryAll(selector string, eval evalFunc) ([]*ElementHand for _, prop := range props { if el := prop.AsElement(); el != nil { els = append(els, el) - } else { - prop.Dispose() + } else if err := prop.Dispose(); err != nil { + return nil, fmt.Errorf("disposing property while querying all selectors %q: %w", selector, err) } } @@ -1111,34 +1200,35 @@ func (h *ElementHandle) queryAll(selector string, eval evalFunc) ([]*ElementHand } // SetChecked checks or unchecks an element. -func (h *ElementHandle) SetChecked(checked bool, opts goja.Value) { - parsedOpts := NewElementHandleSetCheckedOptions(h.defaultTimeout()) - err := parsedOpts.Parse(h.ctx, opts) - if err != nil { - k6ext.Panic(h.ctx, "parsing setChecked options: %w", err) +func (h *ElementHandle) SetChecked(checked bool, opts sobek.Value) error { + popts := NewElementHandleSetCheckedOptions(h.defaultTimeout()) + if err := popts.Parse(h.ctx, opts); err != nil { + return fmt.Errorf("parsing setChecked options: %w", err) } - fn := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) { + setChecked := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) { return nil, handle.setChecked(apiCtx, checked, p) } - pointerFn := h.newPointerAction(fn, &parsedOpts.ElementHandleBasePointerOptions) - _, err = call(h.ctx, pointerFn, parsedOpts.Timeout) - if err != nil { - k6ext.Panic(h.ctx, "checking element: %w", err) + setCheckedAction := h.newPointerAction(setChecked, &popts.ElementHandleBasePointerOptions) + if _, err := call(h.ctx, setCheckedAction, popts.Timeout); err != nil { + return fmt.Errorf("checking element: %w", err) } + applySlowMo(h.ctx) + + return nil } // Uncheck scrolls element into view, and if it's an input element of type // checkbox that is already checked, clicks on it to mark it as unchecked. -func (h *ElementHandle) Uncheck(opts goja.Value) { - h.SetChecked(false, opts) +func (h *ElementHandle) Uncheck(opts sobek.Value) error { + return h.SetChecked(false, opts) } // Check scrolls element into view, and if it's an input element of type // checkbox that is unchecked, clicks on it to mark it as checked. -func (h *ElementHandle) Check(opts goja.Value) { - h.SetChecked(true, opts) +func (h *ElementHandle) Check(opts sobek.Value) error { + return h.SetChecked(true, opts) } func (h *ElementHandle) setChecked(apiCtx context.Context, checked bool, p *Position) error { @@ -1191,61 +1281,75 @@ func (h *ElementHandle) Screenshot( return buf, err } -func (h *ElementHandle) ScrollIntoViewIfNeeded(opts goja.Value) { - actionOpts := NewElementHandleBaseOptions(h.defaultTimeout()) - if err := actionOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing scrollIntoViewIfNeeded options: %w", err) +// ScrollIntoViewIfNeeded scrolls element into view if needed. +func (h *ElementHandle) ScrollIntoViewIfNeeded(opts sobek.Value) error { + aopts := NewElementHandleBaseOptions(h.defaultTimeout()) + if err := aopts.Parse(h.ctx, opts); err != nil { + return fmt.Errorf("parsing scrollIntoViewIfNeeded options: %w", err) } - err := h.waitAndScrollIntoViewIfNeeded(h.ctx, actionOpts.Force, actionOpts.NoWaitAfter, actionOpts.Timeout) + + err := h.waitAndScrollIntoViewIfNeeded(h.ctx, aopts.Force, aopts.NoWaitAfter, aopts.Timeout) if err != nil { - k6ext.Panic(h.ctx, "scrolling element into view: %w", err) + return fmt.Errorf("scrolling element into view: %w", err) } + applySlowMo(h.ctx) + + return nil } -func (h *ElementHandle) SelectOption(values goja.Value, opts goja.Value) []string { - actionOpts := NewElementHandleBaseOptions(h.defaultTimeout()) - if err := actionOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing selectOption options: %w", err) +// SelectOption selects the options matching the given values. +func (h *ElementHandle) SelectOption(values sobek.Value, opts sobek.Value) ([]string, error) { + aopts := NewElementHandleBaseOptions(h.defaultTimeout()) + if err := aopts.Parse(h.ctx, opts); err != nil { + return nil, fmt.Errorf("parsing selectOption options: %w", err) } - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { + + selectOption := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.selectOption(apiCtx, values) } - actFn := h.newAction([]string{}, fn, actionOpts.Force, actionOpts.NoWaitAfter, actionOpts.Timeout) - selectedOptions, err := call(h.ctx, actFn, actionOpts.Timeout) + selectOptionAction := h.newAction( + []string{}, selectOption, aopts.Force, aopts.NoWaitAfter, aopts.Timeout, + ) + selectedOptions, err := call(h.ctx, selectOptionAction, aopts.Timeout) if err != nil { - k6ext.Panic(h.ctx, "selecting options: %w", err) + return nil, fmt.Errorf("selecting options: %w", err) } var returnVal []string if err := convert(selectedOptions, &returnVal); err != nil { - k6ext.Panic(h.ctx, "unpacking selected options: %w", err) + return nil, fmt.Errorf("unpacking selected options: %w", err) } applySlowMo(h.ctx) - return returnVal + return returnVal, nil } -func (h *ElementHandle) SelectText(opts goja.Value) { - actionOpts := NewElementHandleBaseOptions(h.defaultTimeout()) - if err := actionOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing selectText options: %w", err) +// SelectText selects the text of the element. +func (h *ElementHandle) SelectText(opts sobek.Value) error { + aopts := NewElementHandleBaseOptions(h.defaultTimeout()) + if err := aopts.Parse(h.ctx, opts); err != nil { + return fmt.Errorf("parsing selectText options: %w", err) } - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { + selectText := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return nil, handle.selectText(apiCtx) } - actFn := h.newAction([]string{}, fn, actionOpts.Force, actionOpts.NoWaitAfter, actionOpts.Timeout) - _, err := call(h.ctx, actFn, actionOpts.Timeout) - if err != nil { - k6ext.Panic(h.ctx, "selecting text: %w", err) + selectTextAction := h.newAction( + []string{}, selectText, aopts.Force, aopts.NoWaitAfter, aopts.Timeout, + ) + if _, err := call(h.ctx, selectTextAction, aopts.Timeout); err != nil { + return fmt.Errorf("selecting text: %w", err) } + applySlowMo(h.ctx) + + return nil } // SetInputFiles sets the given files into the input file element. -func (h *ElementHandle) SetInputFiles(files goja.Value, opts goja.Value) error { - actionOpts := NewElementHandleSetInputFilesOptions(h.defaultTimeout()) - if err := actionOpts.Parse(h.ctx, opts); err != nil { +func (h *ElementHandle) SetInputFiles(files sobek.Value, opts sobek.Value) error { + aopts := NewElementHandleSetInputFilesOptions(h.defaultTimeout()) + if err := aopts.Parse(h.ctx, opts); err != nil { return fmt.Errorf("parsing setInputFiles options: %w", err) } @@ -1255,12 +1359,11 @@ func (h *ElementHandle) SetInputFiles(files goja.Value, opts goja.Value) error { return fmt.Errorf("parsing setInputFiles parameter: %w", err) } - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { + setInputFiles := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return nil, handle.setInputFiles(apiCtx, actionParam.Payload) } - actFn := h.newAction([]string{}, fn, actionOpts.Force, actionOpts.NoWaitAfter, actionOpts.Timeout) - _, err := call(h.ctx, actFn, actionOpts.Timeout) - if err != nil { + setInputFilesAction := h.newAction([]string{}, setInputFiles, aopts.Force, aopts.NoWaitAfter, aopts.Timeout) + if _, err := call(h.ctx, setInputFilesAction, aopts.Timeout); err != nil { return fmt.Errorf("setting input files: %w", err) } @@ -1312,21 +1415,32 @@ func (h *ElementHandle) tap(_ context.Context, p *Position) error { return h.frame.page.Touchscreen.tap(p.X, p.Y) } -func (h *ElementHandle) TextContent() string { - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { +// TextContent returns the text content of the element. +// The second return value is true if the text content exists, and false otherwise. +func (h *ElementHandle) TextContent() (string, bool, error) { + textContent := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.textContent(apiCtx) } opts := NewElementHandleBaseOptions(h.defaultTimeout()) - actFn := h.newAction([]string{}, fn, opts.Force, opts.NoWaitAfter, opts.Timeout) - v, err := call(h.ctx, actFn, opts.Timeout) + textContentAction := h.newAction( + []string{}, textContent, opts.Force, opts.NoWaitAfter, opts.Timeout, + ) + v, err := call(h.ctx, textContentAction, opts.Timeout) if err != nil { - k6ext.Panic(h.ctx, "getting text content of element: %w", err) + return "", false, fmt.Errorf("getting text content of element: %w", err) + } + if v == nil { + return "", false, nil + } + s, ok := v.(string) + if !ok { + return "", false, fmt.Errorf( + "getting text content of element: unexpected type %T (expecting string)", + v, + ) } - applySlowMo(h.ctx) - - // TODO: handle error - return v.(string) //nolint:forcetypeassert + return s, true, nil } // Timeout will return the default timeout or the one set by the user. @@ -1336,36 +1450,43 @@ func (h *ElementHandle) Timeout() time.Duration { } // Type scrolls element into view, focuses element and types text. -func (h *ElementHandle) Type(text string, opts goja.Value) { - parsedOpts := NewElementHandleTypeOptions(h.defaultTimeout()) - if err := parsedOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing type options: %v", err) +func (h *ElementHandle) Type(text string, opts sobek.Value) error { + popts := NewElementHandleTypeOptions(h.defaultTimeout()) + if err := popts.Parse(h.ctx, opts); err != nil { + return fmt.Errorf("parsing type options: %w", err) } - fn := func(apiCtx context.Context, handle *ElementHandle) (any, error) { + + typ := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return nil, handle.typ(apiCtx, text, NewKeyboardOptions()) } - actFn := h.newAction([]string{}, fn, false, parsedOpts.NoWaitAfter, parsedOpts.Timeout) - _, err := call(h.ctx, actFn, parsedOpts.Timeout) - if err != nil { - k6ext.Panic(h.ctx, "typing text %q: %w", text, err) + typeAction := h.newAction( + []string{}, typ, false, popts.NoWaitAfter, popts.Timeout, + ) + if _, err := call(h.ctx, typeAction, popts.Timeout); err != nil { + return fmt.Errorf("typing text %q: %w", text, err) } + applySlowMo(h.ctx) + + return nil } -func (h *ElementHandle) WaitForElementState(state string, opts goja.Value) { - parsedOpts := NewElementHandleWaitForElementStateOptions(h.defaultTimeout()) - err := parsedOpts.Parse(h.ctx, opts) - if err != nil { - k6ext.Panic(h.ctx, "parsing waitForElementState options: %w", err) +// WaitForElementState waits for the element to reach the given state. +func (h *ElementHandle) WaitForElementState(state string, opts sobek.Value) error { + popts := NewElementHandleWaitForElementStateOptions(h.defaultTimeout()) + if err := popts.Parse(h.ctx, opts); err != nil { + return fmt.Errorf("parsing waitForElementState options: %w", err) } - _, err = h.waitForElementState(h.ctx, []string{state}, parsedOpts.Timeout) + _, err := h.waitForElementState(h.ctx, []string{state}, popts.Timeout) if err != nil { - k6ext.Panic(h.ctx, "waiting for element state %q: %w", state, err) + return fmt.Errorf("waiting for element state %q: %w", state, err) } + + return nil } // WaitForSelector waits for the selector to appear in the DOM. -func (h *ElementHandle) WaitForSelector(selector string, opts goja.Value) (*ElementHandle, error) { +func (h *ElementHandle) WaitForSelector(selector string, opts sobek.Value) (*ElementHandle, error) { parsedOpts := NewFrameWaitForSelectorOptions(h.defaultTimeout()) if err := parsedOpts.Parse(h.ctx, opts); err != nil { return nil, fmt.Errorf("parsing waitForSelector %q options: %w", selector, err) @@ -1565,11 +1686,11 @@ func errorFromDOMError(v any) error { serr = e case error: if e == nil { - panic("DOM error is nil") + return errors.New("DOM error is nil") } err, serr = e, e.Error() default: - panic(fmt.Errorf("unexpected DOM error type %T", v)) + return fmt.Errorf("unexpected DOM error type %T", v) } var uerr *k6ext.UserFriendlyError if errors.As(err, &uerr) { diff --git a/vendor/github.com/grafana/xk6-browser/common/element_handle_options.go b/vendor/github.com/grafana/xk6-browser/common/element_handle_options.go index 7ab0be9193c..91417fe7cb7 100644 --- a/vendor/github.com/grafana/xk6-browser/common/element_handle_options.go +++ b/vendor/github.com/grafana/xk6-browser/common/element_handle_options.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/k6ext" ) @@ -38,6 +38,13 @@ const ( ScrollPositionNearest ScrollPosition = "nearest" ) +const ( + optionButton = "button" + optionDelay = "delay" + optionClickCount = "clickCount" + optionModifiers = "modifiers" +) + // ScrollIntoViewOptions change the behavior of ScrollIntoView. // See: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView type ScrollIntoViewOptions struct { @@ -135,8 +142,9 @@ func NewElementHandleBaseOptions(defaultTimeout time.Duration) *ElementHandleBas } } -func (o *ElementHandleBaseOptions) Parse(ctx context.Context, opts goja.Value) error { - if !gojaValueExists(opts) { +// Parse parses the ElementHandleBaseOptions from the given opts. +func (o *ElementHandleBaseOptions) Parse(ctx context.Context, opts sobek.Value) error { + if !sobekValueExists(opts) { return nil } gopts := opts.ToObject(k6ext.Runtime(ctx)) @@ -162,12 +170,13 @@ func NewElementHandleBasePointerOptions(defaultTimeout time.Duration) *ElementHa } } -func (o *ElementHandleBasePointerOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandleBasePointerOptions from the given opts. +func (o *ElementHandleBasePointerOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) if err := o.ElementHandleBaseOptions.Parse(ctx, opts); err != nil { return err } - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -192,7 +201,8 @@ func NewElementHandleCheckOptions(defaultTimeout time.Duration) *ElementHandleCh } } -func (o *ElementHandleCheckOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandleCheckOptions from the given opts. +func (o *ElementHandleCheckOptions) Parse(ctx context.Context, opts sobek.Value) error { return o.ElementHandleBasePointerOptions.Parse(ctx, opts) } @@ -204,8 +214,8 @@ func NewElementHandleSetInputFilesOptions(defaultTimeout time.Duration) *Element } // addFile to the struct. Input value can only be a file descriptor object. -func (f *Files) addFile(ctx context.Context, file goja.Value) error { - if !gojaValueExists(file) { +func (f *Files) addFile(ctx context.Context, file sobek.Value) error { + if !sobekValueExists(file) { return nil } rt := k6ext.Runtime(ctx) @@ -224,10 +234,10 @@ func (f *Files) addFile(ctx context.Context, file goja.Value) error { return nil } -// Parse parses the Files struct from the given goja.Value. -func (f *Files) Parse(ctx context.Context, files goja.Value) error { +// Parse parses the Files struct from the given sobek.Value. +func (f *Files) Parse(ctx context.Context, files sobek.Value) error { rt := k6ext.Runtime(ctx) - if !gojaValueExists(files) { + if !sobekValueExists(files) { return nil } @@ -249,7 +259,7 @@ func (f *Files) Parse(ctx context.Context, files goja.Value) error { } // Parse parses the ElementHandleSetInputFilesOption from the given opts. -func (o *ElementHandleSetInputFilesOptions) Parse(ctx context.Context, opts goja.Value) error { +func (o *ElementHandleSetInputFilesOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -267,30 +277,35 @@ func NewElementHandleClickOptions(defaultTimeout time.Duration) *ElementHandleCl } } -func (o *ElementHandleClickOptions) Parse(ctx context.Context, opts goja.Value) error { - rt := k6ext.Runtime(ctx) +// Parse parses the ElementHandleClickOptions from the given opts. +func (o *ElementHandleClickOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleBasePointerOptions.Parse(ctx, opts); err != nil { return err } - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { - opts := opts.ToObject(rt) - for _, k := range opts.Keys() { - switch k { - case "button": - o.Button = opts.Get(k).String() - case "clickCount": - o.ClickCount = opts.Get(k).ToInteger() - case "delay": - o.Delay = opts.Get(k).ToInteger() - case "modifiers": - var m []string - if err := rt.ExportTo(opts.Get(k), &m); err != nil { - return err - } - o.Modifiers = m + + if !sobekValueExists(opts) { + return nil + } + + rt := k6ext.Runtime(ctx) + obj := opts.ToObject(rt) + for _, k := range obj.Keys() { + switch k { + case optionButton: + o.Button = obj.Get(k).String() + case optionClickCount: + o.ClickCount = obj.Get(k).ToInteger() + case optionDelay: + o.Delay = obj.Get(k).ToInteger() + case optionModifiers: + var m []string + if err := rt.ExportTo(obj.Get(k), &m); err != nil { + return fmt.Errorf("parsing element handle click option modifiers: %w", err) } + o.Modifiers = m } } + return nil } @@ -311,12 +326,13 @@ func NewElementHandleDblclickOptions(defaultTimeout time.Duration) *ElementHandl } } -func (o *ElementHandleDblclickOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandleDblclickOptions from the given opts. +func (o *ElementHandleDblclickOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) if err := o.ElementHandleBasePointerOptions.Parse(ctx, opts); err != nil { return err } - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -351,12 +367,13 @@ func NewElementHandleHoverOptions(defaultTimeout time.Duration) *ElementHandleHo } } -func (o *ElementHandleHoverOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandleHoverOptions from the given opts. +func (o *ElementHandleHoverOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) if err := o.ElementHandleBasePointerOptions.Parse(ctx, opts); err != nil { return err } - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -380,9 +397,10 @@ func NewElementHandlePressOptions(defaultTimeout time.Duration) *ElementHandlePr } } -func (o *ElementHandlePressOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandlePressOptions from the given opts. +func (o *ElementHandlePressOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -416,36 +434,40 @@ func NewElementHandleScreenshotOptions(defaultTimeout time.Duration) *ElementHan } } -func (o *ElementHandleScreenshotOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandleScreenshotOptions from the given opts. +func (o *ElementHandleScreenshotOptions) Parse(ctx context.Context, opts sobek.Value) error { //nolint:cyclop + if !sobekValueExists(opts) { + return nil + } + rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { - formatSpecified := false - opts := opts.ToObject(rt) - for _, k := range opts.Keys() { - switch k { - case "omitBackground": - o.OmitBackground = opts.Get(k).ToBoolean() - case "path": - o.Path = opts.Get(k).String() - case "quality": - o.Quality = opts.Get(k).ToInteger() - case "type": - if f, ok := imageFormatToID[opts.Get(k).String()]; ok { - o.Format = f - formatSpecified = true - } - case "timeout": - o.Timeout = time.Duration(opts.Get(k).ToInteger()) * time.Millisecond + formatSpecified := false + obj := opts.ToObject(rt) + for _, k := range obj.Keys() { + switch k { + case "omitBackground": + o.OmitBackground = obj.Get(k).ToBoolean() + case "path": + o.Path = obj.Get(k).String() + case "quality": + o.Quality = obj.Get(k).ToInteger() + case "type": + if f, ok := imageFormatToID[obj.Get(k).String()]; ok { + o.Format = f + formatSpecified = true } + case "timeout": + o.Timeout = time.Duration(obj.Get(k).ToInteger()) * time.Millisecond } + } - // Infer file format by path if format not explicitly specified (default is PNG) - if o.Path != "" && !formatSpecified { - if strings.HasSuffix(o.Path, ".jpg") || strings.HasSuffix(o.Path, ".jpeg") { - o.Format = ImageFormatJPEG - } + // Infer file format by path if format not explicitly specified (default is PNG) + if o.Path != "" && !formatSpecified { + if strings.HasSuffix(o.Path, ".jpg") || strings.HasSuffix(o.Path, ".jpeg") { + o.Format = ImageFormatJPEG } } + return nil } @@ -456,14 +478,15 @@ func NewElementHandleSetCheckedOptions(defaultTimeout time.Duration) *ElementHan } } -func (o *ElementHandleSetCheckedOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandleSetCheckedOptions from the given opts. +func (o *ElementHandleSetCheckedOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) if err := o.ElementHandleBasePointerOptions.Parse(ctx, opts); err != nil { return err } - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -482,12 +505,13 @@ func NewElementHandleTapOptions(defaultTimeout time.Duration) *ElementHandleTapO } } -func (o *ElementHandleTapOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandleTapOptions from the given opts. +func (o *ElementHandleTapOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) if err := o.ElementHandleBasePointerOptions.Parse(ctx, opts); err != nil { return err } - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -511,9 +535,10 @@ func NewElementHandleTypeOptions(defaultTimeout time.Duration) *ElementHandleTyp } } -func (o *ElementHandleTypeOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandleTypeOptions from the given opts. +func (o *ElementHandleTypeOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -543,9 +568,10 @@ func NewElementHandleWaitForElementStateOptions(defaultTimeout time.Duration) *E } } -func (o *ElementHandleWaitForElementStateOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the ElementHandleWaitForElementStateOptions from the given opts. +func (o *ElementHandleWaitForElementStateOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { diff --git a/vendor/github.com/grafana/xk6-browser/common/execution_context.go b/vendor/github.com/grafana/xk6-browser/common/execution_context.go index 48106a1c26e..749897e8b87 100644 --- a/vendor/github.com/grafana/xk6-browser/common/execution_context.go +++ b/vendor/github.com/grafana/xk6-browser/common/execution_context.go @@ -157,8 +157,8 @@ func (e *ExecutionContext) adoptElementHandle(eh *ElementHandle) (*ElementHandle func (e *ExecutionContext) eval( apiCtx context.Context, opts evalOptions, js string, args ...any, ) (any, error) { - if escapesGojaValues(args...) { - return nil, errors.New("goja.Value escaped") + if escapesSobekValues(args...) { + return nil, errors.New("sobek.Value escaped") } e.logger.Debugf( "ExecutionContext:eval", @@ -299,8 +299,8 @@ func (e *ExecutionContext) getInjectedScript(apiCtx context.Context) (JSHandleAP // Eval evaluates the provided JavaScript within this execution context and // returns a value or handle. func (e *ExecutionContext) Eval(apiCtx context.Context, js string, args ...any) (any, error) { - if escapesGojaValues(args...) { - return nil, errors.New("goja.Value escaped") + if escapesSobekValues(args...) { + return nil, errors.New("sobek.Value escaped") } opts := evalOptions{ forceCallable: true, @@ -317,8 +317,8 @@ func (e *ExecutionContext) Eval(apiCtx context.Context, js string, args ...any) // EvalHandle evaluates the provided JavaScript within this execution context // and returns a JSHandle. func (e *ExecutionContext) EvalHandle(apiCtx context.Context, js string, args ...any) (JSHandleAPI, error) { - if escapesGojaValues(args...) { - return nil, errors.New("goja.Value escaped") + if escapesSobekValues(args...) { + return nil, errors.New("sobek.Value escaped") } opts := evalOptions{ forceCallable: true, diff --git a/vendor/github.com/grafana/xk6-browser/common/frame.go b/vendor/github.com/grafana/xk6-browser/common/frame.go index 2bd95ca15da..6e2aa2365d2 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame.go @@ -9,15 +9,15 @@ import ( "sync" "time" + "github.com/chromedp/cdproto/cdp" + "github.com/chromedp/cdproto/network" + "github.com/chromedp/cdproto/runtime" + "github.com/grafana/sobek" + "github.com/grafana/xk6-browser/k6ext" "github.com/grafana/xk6-browser/log" k6modules "go.k6.io/k6/js/modules" - - "github.com/chromedp/cdproto/cdp" - "github.com/chromedp/cdproto/network" - "github.com/chromedp/cdproto/runtime" - "github.com/dop251/goja" ) // maxRetry controls how many times to retry if an action fails. @@ -228,7 +228,7 @@ func (f *Frame) clearLifecycle() { f.inflightRequestsMu.Unlock() } -func (f *Frame) detach() { +func (f *Frame) detach() error { f.log.Debugf("Frame:detach", "fid:%s furl:%q", f.ID(), f.URL()) f.setDetached(true) @@ -236,12 +236,18 @@ func (f *Frame) detach() { f.parentFrame.removeChildFrame(f) } f.parentFrame = nil + // detach() is called by the same frame Goroutine that manages execution // context switches. so this should be safe. // we don't need to protect the following with executionContextMu. - if f.documentHandle != nil { - f.documentHandle.Dispose() + if f.documentHandle == nil { + return nil + } + if err := f.documentHandle.Dispose(); err != nil { + return fmt.Errorf("disposing document handle while detaching frame: %w", err) } + + return nil } func (f *Frame) defaultTimeout() time.Duration { @@ -516,7 +522,7 @@ func (f *Frame) waitForSelectorRetry( return nil, err } -func (f *Frame) waitForSelector(selector string, opts *FrameWaitForSelectorOptions) (*ElementHandle, error) { +func (f *Frame) waitForSelector(selector string, opts *FrameWaitForSelectorOptions) (_ *ElementHandle, rerr error) { f.log.Debugf("Frame:waitForSelector", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) document, err := f.document() @@ -543,9 +549,14 @@ func (f *Frame) waitForSelector(selector string, opts *FrameWaitForSelectorOptio // an element should belong to the current execution context. // otherwise, we should adopt it to this execution context. if ec != handle.execCtx { - defer handle.Dispose() + defer func() { + if err := handle.Dispose(); err != nil { + err = fmt.Errorf("disposing element handle: %w", err) + rerr = errors.Join(err, rerr) + } + }() if handle, err = ec.adoptElementHandle(handle); err != nil { - return nil, fmt.Errorf("adopting element handle while waiting for selector %q: %w", selector, err) + return nil, fmt.Errorf("waiting for selector %q: adopting element handle: %w", selector, err) } } @@ -604,17 +615,20 @@ func (f *Frame) click(selector string, opts *FrameClickOptions) error { } // Check clicks the first element found that matches selector. -func (f *Frame) Check(selector string, opts goja.Value) { +func (f *Frame) Check(selector string, opts sobek.Value) error { f.log.Debugf("Frame:Check", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameCheckOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing new frame check options: %w", err) + return fmt.Errorf("parsing new frame check options: %w", err) } if err := f.check(selector, popts); err != nil { - k6ext.Panic(f.ctx, "checking %q: %w", selector, err) + return fmt.Errorf("checking %q: %w", selector, err) } + applySlowMo(f.ctx) + + return nil } func (f *Frame) check(selector string, opts *FrameCheckOptions) error { @@ -632,17 +646,20 @@ func (f *Frame) check(selector string, opts *FrameCheckOptions) error { } // Uncheck the first found element that matches the selector. -func (f *Frame) Uncheck(selector string, opts goja.Value) { +func (f *Frame) Uncheck(selector string, opts sobek.Value) error { f.log.Debugf("Frame:Uncheck", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameUncheckOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing frame uncheck options %q: %w", selector, err) + return fmt.Errorf("parsing frame uncheck options %q: %w", selector, err) } if err := f.uncheck(selector, popts); err != nil { - k6ext.Panic(f.ctx, "unchecking %q: %w", selector, err) + return fmt.Errorf("unchecking %q: %w", selector, err) } + applySlowMo(f.ctx) + + return nil } func (f *Frame) uncheck(selector string, opts *FrameUncheckOptions) error { @@ -661,19 +678,19 @@ func (f *Frame) uncheck(selector string, opts *FrameUncheckOptions) error { // IsChecked returns true if the first element that matches the selector // is checked. Otherwise, returns false. -func (f *Frame) IsChecked(selector string, opts goja.Value) bool { +func (f *Frame) IsChecked(selector string, opts sobek.Value) (bool, error) { f.log.Debugf("Frame:IsChecked", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameIsCheckedOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing is checked options: %w", err) + return false, fmt.Errorf("parsing is checked options: %w", err) } checked, err := f.isChecked(selector, popts) if err != nil { - k6ext.Panic(f.ctx, "checking element is checked %q: %w", selector, err) + return false, fmt.Errorf("checking element is checked %q: %w", selector, err) } - return checked + return checked, nil } func (f *Frame) isChecked(selector string, opts *FrameIsCheckedOptions) (bool, error) { @@ -701,7 +718,7 @@ func (f *Frame) isChecked(selector string, opts *FrameIsCheckedOptions) (bool, e } // Content returns the HTML content of the frame. -func (f *Frame) Content() string { +func (f *Frame) Content() (string, error) { f.log.Debugf("Frame:Content", "fid:%s furl:%q", f.ID(), f.URL()) js := `() => { @@ -715,30 +732,40 @@ func (f *Frame) Content() string { return content; }` - // TODO: return error + v, err := f.Evaluate(js) + if err != nil { + return "", fmt.Errorf("getting frame content: %w", err) + } + s, ok := v.(string) + if !ok { + return "", fmt.Errorf("getting frame content: expected string, got %T", v) + } - return f.Evaluate(js).(string) //nolint:forcetypeassert + return s, nil } // Dblclick double clicks an element matching provided selector. -func (f *Frame) Dblclick(selector string, opts goja.Value) { +func (f *Frame) Dblclick(selector string, opts sobek.Value) error { f.log.Debugf("Frame:DblClick", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameDblClickOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing double click options: %w", err) + return fmt.Errorf("parsing double click options: %w", err) } if err := f.dblclick(selector, popts); err != nil { - k6ext.Panic(f.ctx, "double clicking on %q: %w", selector, err) + return fmt.Errorf("double clicking on %q: %w", selector, err) } + applySlowMo(f.ctx) + + return nil } // dblclick is like Dblclick but takes parsed options and neither throws // an error, or applies slow motion. func (f *Frame) dblclick(selector string, opts *FrameDblclickOptions) error { - dblclick := func(apiCtx context.Context, eh *ElementHandle, p *Position) (any, error) { - return nil, eh.dblClick(p, opts.ToMouseClickOptions()) + dblclick := func(_ context.Context, eh *ElementHandle, p *Position) (any, error) { + return nil, eh.dblclick(p, opts.ToMouseClickOptions()) } act := f.newPointerAction( selector, DOMElementStateAttached, opts.Strict, dblclick, &opts.ElementHandleBasePointerOptions, @@ -785,7 +812,7 @@ func (f *Frame) dispatchEvent(selector, typ string, eventInit any, opts *FrameDi // EvaluateWithContext will evaluate provided page function within an execution context. // The passed in context will be used instead of the frame's context. The context must -// be a derivative of one that contains the goja runtime. +// be a derivative of one that contains the sobek runtime. func (f *Frame) EvaluateWithContext(ctx context.Context, pageFunc string, args ...any) (any, error) { f.log.Debugf("Frame:EvaluateWithContext", "fid:%s furl:%q", f.ID(), f.URL()) @@ -806,15 +833,10 @@ func (f *Frame) EvaluateWithContext(ctx context.Context, pageFunc string, args . } // Evaluate will evaluate provided page function within an execution context. -func (f *Frame) Evaluate(pageFunc string, args ...any) any { +func (f *Frame) Evaluate(pageFunc string, args ...any) (any, error) { f.log.Debugf("Frame:Evaluate", "fid:%s furl:%q", f.ID(), f.URL()) - result, err := f.EvaluateWithContext(f.ctx, pageFunc, args...) - if err != nil { - k6ext.Panic(f.ctx, "%v", err) - } - - return result + return f.EvaluateWithContext(f.ctx, pageFunc, args...) } // EvaluateGlobal will evaluate the given JS code in the global object. @@ -839,18 +861,19 @@ func (f *Frame) EvaluateGlobal(ctx context.Context, js string) error { func (f *Frame) EvaluateHandle(pageFunc string, args ...any) (handle JSHandleAPI, _ error) { f.log.Debugf("Frame:EvaluateHandle", "fid:%s furl:%q", f.ID(), f.URL()) - f.waitForExecutionContext(mainWorld) + evalHandle := func() (JSHandleAPI, error) { + f.executionContextMu.RLock() + defer f.executionContextMu.RUnlock() - var err error - f.executionContextMu.RLock() - { ec := f.executionContexts[mainWorld] if ec == nil { - k6ext.Panic(f.ctx, "evaluating handle for frame: execution context %q not found", mainWorld) + return nil, fmt.Errorf("evaluating handle for frame: execution context %q not found", mainWorld) } - handle, err = ec.EvalHandle(f.ctx, pageFunc, args...) + return ec.EvalHandle(f.ctx, pageFunc, args...) //nolint:wrapcheck } - f.executionContextMu.RUnlock() + + f.waitForExecutionContext(mainWorld) + handle, err := evalHandle() if err != nil { return nil, fmt.Errorf("evaluating handle for frame: %w", err) } @@ -861,17 +884,20 @@ func (f *Frame) EvaluateHandle(pageFunc string, args ...any) (handle JSHandleAPI } // Fill fills out the first element found that matches the selector. -func (f *Frame) Fill(selector, value string, opts goja.Value) { +func (f *Frame) Fill(selector, value string, opts sobek.Value) error { f.log.Debugf("Frame:Fill", "fid:%s furl:%q sel:%q val:%q", f.ID(), f.URL(), selector, value) popts := NewFrameFillOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing fill options: %w", err) + return fmt.Errorf("parsing fill options: %w", err) } if err := f.fill(selector, value, popts); err != nil { - k6ext.Panic(f.ctx, "filling %q with %q: %w", selector, value, err) + return fmt.Errorf("filling %q with %q: %w", selector, value, err) } + applySlowMo(f.ctx) + + return nil } func (f *Frame) fill(selector, value string, opts *FrameFillOptions) error { @@ -891,17 +917,20 @@ func (f *Frame) fill(selector, value string, opts *FrameFillOptions) error { } // Focus focuses on the first element that matches the selector. -func (f *Frame) Focus(selector string, opts goja.Value) { +func (f *Frame) Focus(selector string, opts sobek.Value) error { f.log.Debugf("Frame:Focus", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameBaseOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing focus options: %w", err) + return fmt.Errorf("parsing focus options: %w", err) } if err := f.focus(selector, popts); err != nil { - k6ext.Panic(f.ctx, "focusing %q: %w", selector, err) + return fmt.Errorf("focusing %q: %w", selector, err) } + applySlowMo(f.ctx) + + return nil } func (f *Frame) focus(selector string, opts *FrameBaseOptions) error { @@ -931,24 +960,23 @@ func (f *Frame) FrameElement() (*ElementHandle, error) { } // GetAttribute of the first element found that matches the selector. -func (f *Frame) GetAttribute(selector, name string, opts goja.Value) any { +// The second return value is true if the attribute exists, and false otherwise. +func (f *Frame) GetAttribute(selector, name string, opts sobek.Value) (string, bool, error) { f.log.Debugf("Frame:GetAttribute", "fid:%s furl:%q sel:%q name:%s", f.ID(), f.URL(), selector, name) popts := NewFrameBaseOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parse: %w", err) + return "", false, fmt.Errorf("parsing get attribute options: %w", err) } - v, err := f.getAttribute(selector, name, popts) + s, ok, err := f.getAttribute(selector, name, popts) if err != nil { - k6ext.Panic(f.ctx, "getting attribute %q of %q: %w", name, selector, err) + return "", false, fmt.Errorf("getting attribute %q of %q: %w", name, selector, err) } - applySlowMo(f.ctx) - - return v + return s, ok, nil } -func (f *Frame) getAttribute(selector, name string, opts *FrameBaseOptions) (any, error) { +func (f *Frame) getAttribute(selector, name string, opts *FrameBaseOptions) (string, bool, error) { getAttribute := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.getAttribute(apiCtx, name) } @@ -958,10 +986,17 @@ func (f *Frame) getAttribute(selector, name string, opts *FrameBaseOptions) (any ) v, err := call(f.ctx, act, opts.Timeout) if err != nil { - return "", errorFromDOMError(err) + return "", false, errorFromDOMError(err) + } + if v == nil { + return "", false, nil + } + s, ok := v.(string) + if !ok { + return "", false, fmt.Errorf("unexpected type %T (expecting string)", v) } - return v, nil + return s, true, nil } // Referrer returns the referrer of the frame from the network manager @@ -996,18 +1031,20 @@ func (f *Frame) Goto(url string, opts *FrameGotoOptions) (*Response, error) { } // Hover moves the pointer over the first element that matches the selector. -func (f *Frame) Hover(selector string, opts goja.Value) { +func (f *Frame) Hover(selector string, opts sobek.Value) error { f.log.Debugf("Frame:Hover", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameHoverOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing hover options: %w", err) + return fmt.Errorf("parsing hover options: %w", err) } if err := f.hover(selector, popts); err != nil { - k6ext.Panic(f.ctx, "hovering %q: %w", selector, err) + return fmt.Errorf("hovering %q: %w", selector, err) } applySlowMo(f.ctx) + + return nil } func (f *Frame) hover(selector string, opts *FrameHoverOptions) error { @@ -1026,21 +1063,19 @@ func (f *Frame) hover(selector string, opts *FrameHoverOptions) error { // InnerHTML returns the innerHTML attribute of the first element found // that matches the selector. -func (f *Frame) InnerHTML(selector string, opts goja.Value) string { +func (f *Frame) InnerHTML(selector string, opts sobek.Value) (string, error) { f.log.Debugf("Frame:InnerHTML", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameInnerHTMLOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing inner HTML options: %w", err) + return "", fmt.Errorf("parsing inner HTML options: %w", err) } v, err := f.innerHTML(selector, popts) if err != nil { - k6ext.Panic(f.ctx, "getting inner HTML of %q: %w", selector, err) + return "", fmt.Errorf("getting inner HTML of %q: %w", selector, err) } - applySlowMo(f.ctx) - - return v + return v, nil } func (f *Frame) innerHTML(selector string, opts *FrameInnerHTMLOptions) (string, error) { @@ -1068,21 +1103,19 @@ func (f *Frame) innerHTML(selector string, opts *FrameInnerHTMLOptions) (string, // InnerText returns the inner text of the first element found // that matches the selector. -func (f *Frame) InnerText(selector string, opts goja.Value) string { +func (f *Frame) InnerText(selector string, opts sobek.Value) (string, error) { f.log.Debugf("Frame:InnerText", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameInnerTextOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing inner text options: %w", err) + return "", fmt.Errorf("parsing inner text options: %w", err) } v, err := f.innerText(selector, popts) if err != nil { - k6ext.Panic(f.ctx, "getting inner text of %q: %w", selector, err) + return "", fmt.Errorf("getting inner text of %q: %w", selector, err) } - applySlowMo(f.ctx) - - return v + return v, nil } func (f *Frame) innerText(selector string, opts *FrameInnerTextOptions) (string, error) { @@ -1108,21 +1141,20 @@ func (f *Frame) innerText(selector string, opts *FrameInnerTextOptions) (string, return gv, nil } -// InputValue returns the input value of the first element found -// that matches the selector. -func (f *Frame) InputValue(selector string, opts goja.Value) string { +// InputValue returns the input value of the first element found that matches the selector. +func (f *Frame) InputValue(selector string, opts sobek.Value) (string, error) { f.log.Debugf("Frame:InputValue", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameInputValueOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing input value options: %w", err) + return "", fmt.Errorf("parsing input value options: %w", err) } v, err := f.inputValue(selector, popts) if err != nil { - k6ext.Panic(f.ctx, "getting input value of %q: %w", selector, err) + return "", fmt.Errorf("getting input value of %q: %w", selector, err) } - return v + return v, nil } func (f *Frame) inputValue(selector string, opts *FrameInputValueOptions) (string, error) { @@ -1163,19 +1195,19 @@ func (f *Frame) setDetached(detached bool) { // IsEditable returns true if the first element that matches the selector // is editable. Otherwise, returns false. -func (f *Frame) IsEditable(selector string, opts goja.Value) bool { +func (f *Frame) IsEditable(selector string, opts sobek.Value) (bool, error) { f.log.Debugf("Frame:IsEditable", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameIsEditableOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "%w", err) + return false, fmt.Errorf("parsing is editable options: %w", err) } editable, err := f.isEditable(selector, popts) if err != nil { - k6ext.Panic(f.ctx, "checking is %q editable: %w", selector, err) + return false, fmt.Errorf("checking is %q editable: %w", selector, err) } - return editable + return editable, nil } func (f *Frame) isEditable(selector string, opts *FrameIsEditableOptions) (bool, error) { @@ -1204,19 +1236,19 @@ func (f *Frame) isEditable(selector string, opts *FrameIsEditableOptions) (bool, // IsEnabled returns true if the first element that matches the selector // is enabled. Otherwise, returns false. -func (f *Frame) IsEnabled(selector string, opts goja.Value) bool { +func (f *Frame) IsEnabled(selector string, opts sobek.Value) (bool, error) { f.log.Debugf("Frame:IsEnabled", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameIsEnabledOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing is enabled options: %w", err) + return false, fmt.Errorf("parsing is enabled options: %w", err) } enabled, err := f.isEnabled(selector, popts) if err != nil { - k6ext.Panic(f.ctx, "checking is %q enabled: %w", selector, err) + return false, fmt.Errorf("checking is %q enabled: %w", selector, err) } - return enabled + return enabled, nil } func (f *Frame) isEnabled(selector string, opts *FrameIsEnabledOptions) (bool, error) { @@ -1245,19 +1277,19 @@ func (f *Frame) isEnabled(selector string, opts *FrameIsEnabledOptions) (bool, e // IsDisabled returns true if the first element that matches the selector // is disabled. Otherwise, returns false. -func (f *Frame) IsDisabled(selector string, opts goja.Value) bool { +func (f *Frame) IsDisabled(selector string, opts sobek.Value) (bool, error) { f.log.Debugf("Frame:IsDisabled", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameIsDisabledOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing is disabled options: %w", err) + return false, fmt.Errorf("parsing is disabled options: %w", err) } disabled, err := f.isDisabled(selector, popts) if err != nil { - k6ext.Panic(f.ctx, "checking is %q disabled: %w", selector, err) + return false, fmt.Errorf("checking is %q disabled: %w", selector, err) } - return disabled + return disabled, nil } func (f *Frame) isDisabled(selector string, opts *FrameIsDisabledOptions) (bool, error) { @@ -1286,7 +1318,7 @@ func (f *Frame) isDisabled(selector string, opts *FrameIsDisabledOptions) (bool, // IsHidden returns true if the first element that matches the selector // is hidden. Otherwise, returns false. -func (f *Frame) IsHidden(selector string, opts goja.Value) (bool, error) { +func (f *Frame) IsHidden(selector string, opts sobek.Value) (bool, error) { f.log.Debugf("Frame:IsHidden", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameIsHiddenOptions() @@ -1319,7 +1351,7 @@ func (f *Frame) isHidden(selector string, opts *FrameIsHiddenOptions) (bool, err // IsVisible returns true if the first element that matches the selector // is visible. Otherwise, returns false. -func (f *Frame) IsVisible(selector string, opts goja.Value) (bool, error) { +func (f *Frame) IsVisible(selector string, opts sobek.Value) (bool, error) { f.log.Debugf("Frame:IsVisible", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameIsVisibleOptions() @@ -1359,7 +1391,7 @@ func (f *Frame) ID() string { } // Locator creates and returns a new locator for this frame. -func (f *Frame) Locator(selector string, opts goja.Value) *Locator { +func (f *Frame) Locator(selector string, opts sobek.Value) *Locator { f.log.Debugf("Frame:Locator", "fid:%s furl:%q selector:%q opts:%+v", f.ID(), f.URL(), selector, opts) return NewLocator(f.ctx, selector, f, f.log) @@ -1388,7 +1420,7 @@ func (f *Frame) Query(selector string, strict bool) (*ElementHandle, error) { document, err := f.document() if err != nil { - k6ext.Panic(f.ctx, "getting document: %w", err) + return nil, fmt.Errorf("getting document: %w", err) } return document.Query(selector, strict) } @@ -1399,7 +1431,7 @@ func (f *Frame) QueryAll(selector string) ([]*ElementHandle, error) { document, err := f.document() if err != nil { - k6ext.Panic(f.ctx, "getting document: %w", err) + return nil, fmt.Errorf("getting document: %w", err) } return document.QueryAll(selector) } @@ -1415,18 +1447,20 @@ func (f *Frame) ParentFrame() *Frame { } // Press presses the given key for the first element found that matches the selector. -func (f *Frame) Press(selector, key string, opts goja.Value) { +func (f *Frame) Press(selector, key string, opts sobek.Value) error { f.log.Debugf("Frame:Press", "fid:%s furl:%q sel:%q key:%q", f.ID(), f.URL(), selector, key) popts := NewFramePressOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing press options: %w", err) + return fmt.Errorf("parsing press options: %w", err) } if err := f.press(selector, key, popts); err != nil { - k6ext.Panic(f.ctx, "pressing %q on %q: %w", key, selector, err) + return fmt.Errorf("pressing %q on %q: %w", key, selector, err) } applySlowMo(f.ctx) + + return nil } func (f *Frame) press(selector, key string, opts *FramePressOptions) error { @@ -1446,24 +1480,24 @@ func (f *Frame) press(selector, key string, opts *FramePressOptions) error { // SelectOption selects the given options and returns the array of // option values of the first element found that matches the selector. -func (f *Frame) SelectOption(selector string, values goja.Value, opts goja.Value) []string { +func (f *Frame) SelectOption(selector string, values sobek.Value, opts sobek.Value) ([]string, error) { f.log.Debugf("Frame:SelectOption", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameSelectOptionOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing select option options: %w", err) + return nil, fmt.Errorf("parsing select option options: %w", err) } v, err := f.selectOption(selector, values, popts) if err != nil { - k6ext.Panic(f.ctx, "selecting option on %q: %w", selector, err) + return nil, fmt.Errorf("selecting option on %q: %w", selector, err) } applySlowMo(f.ctx) - return v + return v, nil } -func (f *Frame) selectOption(selector string, values goja.Value, opts *FrameSelectOptionOptions) ([]string, error) { +func (f *Frame) selectOption(selector string, values sobek.Value, opts *FrameSelectOptionOptions) ([]string, error) { selectOption := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.selectOption(apiCtx, values) } @@ -1504,14 +1538,14 @@ func (f *Frame) selectOption(selector string, values goja.Value, opts *FrameSele } // SetContent replaces the entire HTML document content. -func (f *Frame) SetContent(html string, opts goja.Value) { +func (f *Frame) SetContent(html string, opts sobek.Value) error { f.log.Debugf("Frame:SetContent", "fid:%s furl:%q", f.ID(), f.URL()) parsedOpts := NewFrameSetContentOptions( f.manager.timeoutSettings.navigationTimeout(), ) if err := parsedOpts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing set content options: %w", err) + return fmt.Errorf("parsing set content options: %w", err) } js := `(html) => { @@ -1528,14 +1562,16 @@ func (f *Frame) SetContent(html string, opts goja.Value) { returnByValue: true, } if _, err := f.evaluate(f.ctx, utilityWorld, eopts, js, html); err != nil { - k6ext.Panic(f.ctx, "setting content: %w", err) + return fmt.Errorf("setting content: %w", err) } applySlowMo(f.ctx) + + return nil } // SetInputFiles sets input files for the selected element. -func (f *Frame) SetInputFiles(selector string, files goja.Value, opts goja.Value) error { +func (f *Frame) SetInputFiles(selector string, files sobek.Value, opts sobek.Value) error { f.log.Debugf("Frame:SetInputFiles", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameSetInputFilesOptions(f.defaultTimeout()) @@ -1602,25 +1638,24 @@ func (f *Frame) setInputFiles(selector string, files *Files, opts *FrameSetInput } // TextContent returns the textContent attribute of the first element found -// that matches the selector. -func (f *Frame) TextContent(selector string, opts goja.Value) string { +// that matches the selector. The second return value is true if the returned +// text content is not null or empty, and false otherwise. +func (f *Frame) TextContent(selector string, opts sobek.Value) (string, bool, error) { f.log.Debugf("Frame:TextContent", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) popts := NewFrameTextContentOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing text content options: %w", err) + return "", false, fmt.Errorf("parsing text content options: %w", err) } - v, err := f.textContent(selector, popts) + v, ok, err := f.textContent(selector, popts) if err != nil { - k6ext.Panic(f.ctx, "getting text content of %q: %w", selector, err) + return "", false, fmt.Errorf("getting text content of %q: %w", selector, err) } - applySlowMo(f.ctx) - - return v + return v, ok, nil } -func (f *Frame) textContent(selector string, opts *FrameTextContentOptions) (string, error) { +func (f *Frame) textContent(selector string, opts *FrameTextContentOptions) (string, bool, error) { TextContent := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.textContent(apiCtx) } @@ -1630,17 +1665,17 @@ func (f *Frame) textContent(selector string, opts *FrameTextContentOptions) (str ) v, err := call(f.ctx, act, opts.Timeout) if err != nil { - return "", errorFromDOMError(err) + return "", false, errorFromDOMError(err) } if v == nil { - return "", nil + return "", false, nil } - gv, ok := v.(string) + s, ok := v.(string) if !ok { - return "", fmt.Errorf("unexpected type %T", v) + return "", false, fmt.Errorf("unexpected type %T (expecting string)", v) } - return gv, nil + return s, true, nil } // Timeout will return the default timeout or the one set by the user. @@ -1653,26 +1688,29 @@ func (f *Frame) Timeout() time.Duration { func (f *Frame) Title() string { f.log.Debugf("Frame:Title", "fid:%s furl:%q", f.ID(), f.URL()) - v := `() => document.title` + script := `() => document.title` // TODO: return error - return f.Evaluate(v).(string) //nolint:forcetypeassert + v, _ := f.Evaluate(script) + return v.(string) //nolint:forcetypeassert } // Type text on the first element found matches the selector. -func (f *Frame) Type(selector, text string, opts goja.Value) { +func (f *Frame) Type(selector, text string, opts sobek.Value) error { f.log.Debugf("Frame:Type", "fid:%s furl:%q sel:%q text:%q", f.ID(), f.URL(), selector, text) popts := NewFrameTypeOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing type options: %w", err) + return fmt.Errorf("parsing type options: %w", err) } if err := f.typ(selector, text, popts); err != nil { - k6ext.Panic(f.ctx, "typing %q in %q: %w", text, selector, err) + return fmt.Errorf("typing %q in %q: %w", text, selector, err) } applySlowMo(f.ctx) + + return nil } func (f *Frame) typ(selector, text string, opts *FrameTypeOptions) error { @@ -1792,45 +1830,48 @@ func (f *Frame) waitForFunction( // WaitForLoadState waits for the given load state to be reached. // This will unblock if that lifecycle event has already been received. -func (f *Frame) WaitForLoadState(state string, opts goja.Value) { +func (f *Frame) WaitForLoadState(state string, opts sobek.Value) error { f.log.Debugf("Frame:WaitForLoadState", "fid:%s furl:%q state:%s", f.ID(), f.URL(), state) defer f.log.Debugf("Frame:WaitForLoadState:return", "fid:%s furl:%q state:%s", f.ID(), f.URL(), state) - parsedOpts := NewFrameWaitForLoadStateOptions(f.defaultTimeout()) - err := parsedOpts.Parse(f.ctx, opts) - if err != nil { - k6ext.Panic(f.ctx, "parsing waitForLoadState %q options: %v", state, err) + waitForLoadStateOpts := NewFrameWaitForLoadStateOptions(f.defaultTimeout()) + if err := waitForLoadStateOpts.Parse(f.ctx, opts); err != nil { + return fmt.Errorf("parsing waitForLoadState %q options: %w", state, err) } - timeoutCtx, timeoutCancel := context.WithTimeout(f.ctx, parsedOpts.Timeout) + timeoutCtx, timeoutCancel := context.WithTimeout(f.ctx, waitForLoadStateOpts.Timeout) defer timeoutCancel() waitUntil := LifecycleEventLoad if state != "" { - if err = waitUntil.UnmarshalText([]byte(state)); err != nil { - k6ext.Panic(f.ctx, "waiting for load state: %v", err) + if err := waitUntil.UnmarshalText([]byte(state)); err != nil { + return fmt.Errorf("unmarshaling wait for load state %q: %w", state, err) } } - lifecycleEvtCh, lifecycleEvtCancel := createWaitForEventPredicateHandler( - timeoutCtx, f, []string{EventFrameAddLifecycle}, + lifecycleEvent, lifecycleEventCancel := createWaitForEventPredicateHandler( + timeoutCtx, + f, + []string{EventFrameAddLifecycle}, func(data any) bool { if le, ok := data.(FrameLifecycleEvent); ok { return le.Event == waitUntil } return false }) - defer lifecycleEvtCancel() + defer lifecycleEventCancel() if f.hasLifecycleEventFired(waitUntil) { - return + return nil } select { - case <-lifecycleEvtCh: + case <-lifecycleEvent: case <-timeoutCtx.Done(): - k6ext.Panic(f.ctx, "waiting for load state %q: %v", state, err) + return fmt.Errorf("waiting for load state %q: %w", state, timeoutCtx.Err()) } + + return nil } // WaitForNavigation waits for the given navigation lifecycle event to happen. @@ -1922,7 +1963,7 @@ func (f *Frame) WaitForNavigation(opts *FrameWaitForNavigationOptions) (*Respons } // WaitForSelector waits for the given selector to match the waiting criteria. -func (f *Frame) WaitForSelector(selector string, opts goja.Value) (*ElementHandle, error) { +func (f *Frame) WaitForSelector(selector string, opts sobek.Value) (*ElementHandle, error) { parsedOpts := NewFrameWaitForSelectorOptions(f.defaultTimeout()) if err := parsedOpts.Parse(f.ctx, opts); err != nil { return nil, fmt.Errorf("parsing wait for selector %q options: %w", selector, err) diff --git a/vendor/github.com/grafana/xk6-browser/common/frame_manager.go b/vendor/github.com/grafana/xk6-browser/common/frame_manager.go index 67bc43a45ce..ae41ef6960d 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame_manager.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame_manager.go @@ -162,7 +162,7 @@ func (m *FrameManager) frameAttached(frameID cdp.FrameID, parentFrameID cdp.Fram } } -func (m *FrameManager) frameDetached(frameID cdp.FrameID, reason cdppage.FrameDetachedReason) { +func (m *FrameManager) frameDetached(frameID cdp.FrameID, reason cdppage.FrameDetachedReason) error { m.logger.Debugf("FrameManager:frameDetached", "fmid:%d fid:%v", m.ID(), frameID) frame, ok := m.getFrameByID(frameID) @@ -170,7 +170,25 @@ func (m *FrameManager) frameDetached(frameID cdp.FrameID, reason cdppage.FrameDe m.logger.Debugf("FrameManager:frameDetached:return", "fmid:%d fid:%v cannot find frame", m.ID(), frameID) - return + return nil + } + + // This helps prevent an iframe and its child frames from being removed + // when the type of detach is a swap. After this detach event usually + // the iframe navigates, which requires the frames to be present for the + // navigate to work. + fs := m.page.getFrameSession(frameID) + if fs != nil { + m.logger.Debugf("FrameManager:frameDetached:sessionFound", + "fmid:%d fid:%v fsID1:%v fsID2:%v found session for frame", + m.ID(), frameID, fs.session.ID(), m.session.ID()) + + if fs.session.ID() != m.session.ID() { + m.logger.Debugf("FrameManager:frameDetached:notSameSession:return", + "fmid:%d fid:%v event session and frame session do not match", + m.ID(), frameID) + return nil + } } if reason == cdppage.FrameDetachedReasonSwap { @@ -178,11 +196,10 @@ func (m *FrameManager) frameDetached(frameID cdp.FrameID, reason cdppage.FrameDe // frame, we want to keep the current frame which is // still referenced by the (incoming) remote frame, but // remove all its child frames. - m.removeChildFramesRecursively(frame) - return + return m.removeChildFramesRecursively(frame) } - m.removeFramesRecursively(frame) + return m.removeFramesRecursively(frame) } func (m *FrameManager) frameLifecycleEvent(frameID cdp.FrameID, event LifecycleEvent) { @@ -239,7 +256,10 @@ func (m *FrameManager) frameNavigated(frameID cdp.FrameID, parentFrameID cdp.Fra if frame != nil { m.framesMu.Unlock() for _, child := range frame.ChildFrames() { - m.removeFramesRecursively(child) + if err := m.removeFramesRecursively(child); err != nil { + m.framesMu.Lock() + return fmt.Errorf("removing child frames recursively: %w", err) + } } m.framesMu.Lock() } @@ -401,22 +421,30 @@ func (m *FrameManager) getFrameByID(id cdp.FrameID) (*Frame, bool) { return frame, ok } -func (m *FrameManager) removeChildFramesRecursively(frame *Frame) { +func (m *FrameManager) removeChildFramesRecursively(frame *Frame) error { for _, child := range frame.ChildFrames() { - m.removeFramesRecursively(child) + if err := m.removeFramesRecursively(child); err != nil { + return fmt.Errorf("removing child frames recursively: %w", err) + } } + + return nil } -func (m *FrameManager) removeFramesRecursively(frame *Frame) { +func (m *FrameManager) removeFramesRecursively(frame *Frame) error { for _, child := range frame.ChildFrames() { m.logger.Debugf("FrameManager:removeFramesRecursively", "fmid:%d cfid:%v pfid:%v cfname:%s cfurl:%s", m.ID(), child.ID(), frame.ID(), child.Name(), child.URL()) - m.removeFramesRecursively(child) + if err := m.removeFramesRecursively(child); err != nil { + return fmt.Errorf("removing frames recursively: %w", err) + } } - frame.detach() + if err := frame.detach(); err != nil { + return fmt.Errorf("removing frames recursively: detaching frame: %w", err) + } m.framesMu.Lock() m.logger.Debugf("FrameManager:removeFramesRecursively:delParentFrame", @@ -433,6 +461,8 @@ func (m *FrameManager) removeFramesRecursively(frame *Frame) { m.page.emit(EventPageFrameDetached, frame) } + + return nil } func (m *FrameManager) requestFailed(req *Request, canceled bool) { diff --git a/vendor/github.com/grafana/xk6-browser/common/frame_options.go b/vendor/github.com/grafana/xk6-browser/common/frame_options.go index b8dba383c80..ccf4df93e0c 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame_options.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame_options.go @@ -8,7 +8,7 @@ import ( "reflect" "time" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/k6ext" ) @@ -205,9 +205,10 @@ func NewFrameBaseOptions(defaultTimeout time.Duration) *FrameBaseOptions { } } -func (o *FrameBaseOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame base options. +func (o *FrameBaseOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -228,7 +229,8 @@ func NewFrameCheckOptions(defaultTimeout time.Duration) *FrameCheckOptions { } } -func (o *FrameCheckOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame check options. +func (o *FrameCheckOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleBasePointerOptions.Parse(ctx, opts); err != nil { return err } @@ -243,7 +245,8 @@ func NewFrameClickOptions(defaultTimeout time.Duration) *FrameClickOptions { } } -func (o *FrameClickOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame click options. +func (o *FrameClickOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleClickOptions.Parse(ctx, opts); err != nil { return err } @@ -258,7 +261,8 @@ func NewFrameDblClickOptions(defaultTimeout time.Duration) *FrameDblclickOptions } } -func (o *FrameDblclickOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame dblclick options. +func (o *FrameDblclickOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleDblclickOptions.Parse(ctx, opts); err != nil { return err } @@ -273,7 +277,8 @@ func NewFrameFillOptions(defaultTimeout time.Duration) *FrameFillOptions { } } -func (o *FrameFillOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame fill options. +func (o *FrameFillOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -289,9 +294,10 @@ func NewFrameGotoOptions(defaultReferer string, defaultTimeout time.Duration) *F } } -func (o *FrameGotoOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame goto options. +func (o *FrameGotoOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -317,7 +323,8 @@ func NewFrameHoverOptions(defaultTimeout time.Duration) *FrameHoverOptions { } } -func (o *FrameHoverOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame hover options. +func (o *FrameHoverOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleHoverOptions.Parse(ctx, opts); err != nil { return err } @@ -331,7 +338,8 @@ func NewFrameInnerHTMLOptions(defaultTimeout time.Duration) *FrameInnerHTMLOptio } } -func (o *FrameInnerHTMLOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame innerHTML options. +func (o *FrameInnerHTMLOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.FrameBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -344,7 +352,8 @@ func NewFrameInnerTextOptions(defaultTimeout time.Duration) *FrameInnerTextOptio } } -func (o *FrameInnerTextOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame innerText options. +func (o *FrameInnerTextOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.FrameBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -357,7 +366,8 @@ func NewFrameInputValueOptions(defaultTimeout time.Duration) *FrameInputValueOpt } } -func (o *FrameInputValueOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame inputValue options. +func (o *FrameInputValueOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.FrameBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -370,7 +380,8 @@ func NewFrameIsCheckedOptions(defaultTimeout time.Duration) *FrameIsCheckedOptio } } -func (o *FrameIsCheckedOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame isChecked options. +func (o *FrameIsCheckedOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.FrameBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -383,7 +394,8 @@ func NewFrameIsDisabledOptions(defaultTimeout time.Duration) *FrameIsDisabledOpt } } -func (o *FrameIsDisabledOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame isDisabled options. +func (o *FrameIsDisabledOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.FrameBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -396,7 +408,8 @@ func NewFrameIsEditableOptions(defaultTimeout time.Duration) *FrameIsEditableOpt } } -func (o *FrameIsEditableOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame isEditable options. +func (o *FrameIsEditableOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.FrameBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -409,7 +422,8 @@ func NewFrameIsEnabledOptions(defaultTimeout time.Duration) *FrameIsEnabledOptio } } -func (o *FrameIsEnabledOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame isEnabled options. +func (o *FrameIsEnabledOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.FrameBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -421,7 +435,8 @@ func NewFrameIsHiddenOptions() *FrameIsHiddenOptions { return &FrameIsHiddenOptions{} } -func (o *FrameIsHiddenOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses FrameIsHiddenOptions from sobek.Value. +func (o *FrameIsHiddenOptions) Parse(ctx context.Context, opts sobek.Value) error { o.Strict = parseStrict(ctx, opts) return nil } @@ -431,7 +446,8 @@ func NewFrameIsVisibleOptions() *FrameIsVisibleOptions { return &FrameIsVisibleOptions{} } -func (o *FrameIsVisibleOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses FrameIsVisibleOptions from sobek.Value. +func (o *FrameIsVisibleOptions) Parse(ctx context.Context, opts sobek.Value) error { o.Strict = parseStrict(ctx, opts) return nil } @@ -456,7 +472,8 @@ func NewFrameSelectOptionOptions(defaultTimeout time.Duration) *FrameSelectOptio } } -func (o *FrameSelectOptionOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame selectOption options. +func (o *FrameSelectOptionOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -471,10 +488,11 @@ func NewFrameSetContentOptions(defaultTimeout time.Duration) *FrameSetContentOpt } } -func (o *FrameSetContentOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame setContent options. +func (o *FrameSetContentOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -500,8 +518,8 @@ func NewFrameSetInputFilesOptions(defaultTimeout time.Duration) *FrameSetInputFi } } -// Parse parses FrameSetInputFilesOptions from goja.Value. -func (o *FrameSetInputFilesOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses FrameSetInputFilesOptions from sobek.Value. +func (o *FrameSetInputFilesOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleSetInputFilesOptions.Parse(ctx, opts); err != nil { return err } @@ -516,12 +534,13 @@ func NewFrameTapOptions(defaultTimeout time.Duration) *FrameTapOptions { } } -func (o *FrameTapOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame tap options. +func (o *FrameTapOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) if err := o.ElementHandleBasePointerOptions.Parse(ctx, opts); err != nil { return err } - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -545,7 +564,8 @@ func NewFrameTextContentOptions(defaultTimeout time.Duration) *FrameTextContentO } } -func (o *FrameTextContentOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame textContent options. +func (o *FrameTextContentOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.FrameBaseOptions.Parse(ctx, opts); err != nil { return err } @@ -572,7 +592,8 @@ func NewFrameUncheckOptions(defaultTimeout time.Duration) *FrameUncheckOptions { } } -func (o *FrameUncheckOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame uncheck options. +func (o *FrameUncheckOptions) Parse(ctx context.Context, opts sobek.Value) error { if err := o.ElementHandleBasePointerOptions.Parse(ctx, opts); err != nil { return err } @@ -589,31 +610,32 @@ func NewFrameWaitForFunctionOptions(defaultTimeout time.Duration) *FrameWaitForF } // Parse JavaScript waitForFunction options. -func (o *FrameWaitForFunctionOptions) Parse(ctx context.Context, opts goja.Value) error { - rt := k6ext.Runtime(ctx) +func (o *FrameWaitForFunctionOptions) Parse(ctx context.Context, opts sobek.Value) error { + if !sobekValueExists(opts) { + return nil + } - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { - opts := opts.ToObject(rt) - for _, k := range opts.Keys() { - v := opts.Get(k) - switch k { - case "timeout": - o.Timeout = time.Duration(v.ToInteger()) * time.Millisecond - case "polling": - switch v.ExportType().Kind() { //nolint: exhaustive - case reflect.Int64: - o.Polling = PollingInterval - o.Interval = v.ToInteger() - case reflect.String: - if p, ok := pollingTypeToID[v.ToString().String()]; ok { - o.Polling = p - break - } - fallthrough - default: - return fmt.Errorf("wrong polling option value: %q; "+ - `possible values: "raf", "mutation" or number`, v) + rt := k6ext.Runtime(ctx) + obj := opts.ToObject(rt) + for _, k := range obj.Keys() { + v := obj.Get(k) + switch k { + case "timeout": + o.Timeout = time.Duration(v.ToInteger()) * time.Millisecond + case "polling": + switch v.ExportType().Kind() { //nolint: exhaustive + case reflect.Int64: + o.Polling = PollingInterval + o.Interval = v.ToInteger() + case reflect.String: + if p, ok := pollingTypeToID[v.ToString().String()]; ok { + o.Polling = p + break } + fallthrough + default: + return fmt.Errorf("wrong polling option value: %q; "+ + `possible values: "raf", "mutation" or number`, v) } } } @@ -627,9 +649,10 @@ func NewFrameWaitForLoadStateOptions(defaultTimeout time.Duration) *FrameWaitFor } } -func (o *FrameWaitForLoadStateOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame waitForLoadState options. +func (o *FrameWaitForLoadStateOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -649,9 +672,10 @@ func NewFrameWaitForNavigationOptions(defaultTimeout time.Duration) *FrameWaitFo } } -func (o *FrameWaitForNavigationOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame waitForNavigation options. +func (o *FrameWaitForNavigationOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -678,10 +702,11 @@ func NewFrameWaitForSelectorOptions(defaultTimeout time.Duration) *FrameWaitForS } } -func (o *FrameWaitForSelectorOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the frame waitForSelector options. +func (o *FrameWaitForSelectorOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -715,11 +740,11 @@ func NewFrameDispatchEventOptions(defaultTimeout time.Duration) *FrameDispatchEv } } -func parseStrict(ctx context.Context, opts goja.Value) bool { +func parseStrict(ctx context.Context, opts sobek.Value) bool { var strict bool rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { if k == "strict" { diff --git a/vendor/github.com/grafana/xk6-browser/common/frame_session.go b/vendor/github.com/grafana/xk6-browser/common/frame_session.go index 5fa411e7c5c..8fb82115ea3 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame_session.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame_session.go @@ -105,6 +105,10 @@ func NewFrameSession( logger: l, } + if err := cdpruntime.RunIfWaitingForDebugger().Do(cdp.WithExecutor(fs.ctx, fs.session)); err != nil { + return nil, fmt.Errorf("run if waiting for debugger to attach: %w", err) + } + var parentNM *NetworkManager if fs.parent != nil { parentNM = fs.parent.networkManager @@ -402,8 +406,10 @@ func (fs *FrameSession) initFrameTree() error { return fmt.Errorf("got a nil page frame tree") } + // Any new frame may have a child frame, not just mainframes. + fs.handleFrameTree(frameTree, fs.isMainFrame()) + if fs.isMainFrame() { - fs.handleFrameTree(frameTree) fs.initRendererEvents() } return nil @@ -507,7 +513,9 @@ func (fs *FrameSession) initOptions() error { if err := fs.updateGeolocation(true); err != nil { return err } - fs.updateExtraHTTPHeaders(true) + if err := fs.updateExtraHTTPHeaders(true); err != nil { + return err + } var reqIntercept bool if state.Options.BlockedHostnames.Trie != nil || @@ -518,8 +526,12 @@ func (fs *FrameSession) initOptions() error { return err } - fs.updateOffline(true) - fs.updateHTTPCredentials(true) + if err := fs.updateOffline(true); err != nil { + return err + } + if err := fs.updateHTTPCredentials(true); err != nil { + return err + } if err := fs.updateEmulateMedia(true); err != nil { return err } @@ -532,8 +544,6 @@ func (fs *FrameSession) initOptions() error { for (const source of this._crPage._page._evaluateOnNewDocumentSources) promises.push(this._evaluateOnNewDocument(source, 'main'));*/ - optActions = append(optActions, cdpruntime.RunIfWaitingForDebugger()) - for _, action := range optActions { if err := action.Do(cdp.WithExecutor(fs.ctx, fs.session)); err != nil { return fmt.Errorf("internal error while initializing frame %T: %w", action, err) @@ -575,19 +585,19 @@ func (fs *FrameSession) isMainFrame() bool { return fs.targetID == fs.page.targetID } -func (fs *FrameSession) handleFrameTree(frameTree *cdppage.FrameTree) { +func (fs *FrameSession) handleFrameTree(frameTree *cdppage.FrameTree, initialFrame bool) { fs.logger.Debugf("FrameSession:handleFrameTree", - "sid:%v tid:%v", fs.session.ID(), fs.targetID) + "fid:%v sid:%v tid:%v", frameTree.Frame.ID, fs.session.ID(), fs.targetID) if frameTree.Frame.ParentID != "" { fs.onFrameAttached(frameTree.Frame.ID, frameTree.Frame.ParentID) } - fs.onFrameNavigated(frameTree.Frame, true) + fs.onFrameNavigated(frameTree.Frame, initialFrame) if frameTree.ChildFrames == nil { return } for _, child := range frameTree.ChildFrames { - fs.handleFrameTree(child) + fs.handleFrameTree(child, initialFrame) } } @@ -745,7 +755,9 @@ func (fs *FrameSession) onFrameDetached(frameID cdp.FrameID, reason cdppage.Fram "sid:%v tid:%v fid:%v reason:%s", fs.session.ID(), fs.targetID, frameID, reason) - fs.manager.frameDetached(frameID, reason) + if err := fs.manager.frameDetached(frameID, reason); err != nil { + k6ext.Panic(fs.ctx, "handling frameDetached event: %w", err) + } } func (fs *FrameSession) onFrameNavigated(frame *cdp.Frame, initial bool) { @@ -1054,7 +1066,7 @@ func (fs *FrameSession) updateEmulateMedia(initial bool) error { return nil } -func (fs *FrameSession) updateExtraHTTPHeaders(initial bool) { +func (fs *FrameSession) updateExtraHTTPHeaders(initial bool) error { fs.logger.Debugf("NewFrameSession:updateExtraHTTPHeaders", "sid:%v tid:%v", fs.session.ID(), fs.targetID) // Merge extra headers from browser context and page, where page specific headers ake precedence. @@ -1066,8 +1078,12 @@ func (fs *FrameSession) updateExtraHTTPHeaders(initial bool) { mergedHeaders[k] = v } if !initial || len(mergedHeaders) > 0 { - fs.networkManager.SetExtraHTTPHeaders(mergedHeaders) + if err := fs.networkManager.SetExtraHTTPHeaders(mergedHeaders); err != nil { + return fmt.Errorf("updating extra HTTP headers: %w", err) + } } + + return nil } func (fs *FrameSession) updateGeolocation(initial bool) error { @@ -1083,25 +1099,32 @@ func (fs *FrameSession) updateGeolocation(initial bool) error { return fmt.Errorf("%w", err) } } + return nil } -func (fs *FrameSession) updateHTTPCredentials(initial bool) { +func (fs *FrameSession) updateHTTPCredentials(initial bool) error { fs.logger.Debugf("NewFrameSession:updateHttpCredentials", "sid:%v tid:%v", fs.session.ID(), fs.targetID) credentials := fs.page.browserCtx.opts.HttpCredentials if !initial || credentials != nil { - fs.networkManager.Authenticate(credentials) + return fs.networkManager.Authenticate(credentials) } + + return nil } -func (fs *FrameSession) updateOffline(initial bool) { +func (fs *FrameSession) updateOffline(initial bool) error { fs.logger.Debugf("NewFrameSession:updateOffline", "sid:%v tid:%v", fs.session.ID(), fs.targetID) offline := fs.page.browserCtx.opts.Offline if !initial || offline { - fs.networkManager.SetOfflineMode(offline) + if err := fs.networkManager.SetOfflineMode(offline); err != nil { + return fmt.Errorf("updating offline mode for frame %v: %w", fs.targetID, err) + } } + + return nil } func (fs *FrameSession) throttleNetwork(networkProfile NetworkProfile) error { diff --git a/vendor/github.com/grafana/xk6-browser/common/helpers.go b/vendor/github.com/grafana/xk6-browser/common/helpers.go index 4bf49a4426f..8ff709a2903 100644 --- a/vendor/github.com/grafana/xk6-browser/common/helpers.go +++ b/vendor/github.com/grafana/xk6-browser/common/helpers.go @@ -9,7 +9,7 @@ import ( "time" cdpruntime "github.com/chromedp/cdproto/runtime" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/k6ext" ) @@ -36,8 +36,8 @@ func convertBaseJSHandleTypes(ctx context.Context, execCtx *ExecutionContext, ob func convertArgument( ctx context.Context, execCtx *ExecutionContext, arg any, ) (*cdpruntime.CallArgument, error) { - if escapesGojaValues(arg) { - return nil, errors.New("goja.Value escaped") + if escapesSobekValues(arg) { + return nil, errors.New("sobek.Value escaped") } switch a := arg.(type) { case int64: @@ -220,26 +220,10 @@ func TrimQuotes(s string) string { return s } -// 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) -} - -// asGojaValue return v as a goja value. -// panics if v is not a goja value. -func asGojaValue(ctx context.Context, v any) goja.Value { - gv, ok := v.(goja.Value) - if !ok { - k6ext.Panic(ctx, "unexpected type %T", v) - } - return gv -} - -// gojaValueToString returns v as string. -// panics if v is not a goja value. -func gojaValueToString(ctx context.Context, v any) string { - return asGojaValue(ctx, v).String() +// sobekValueExists returns true if a given value is not nil and exists +// (defined and not null) in the sobek runtime. +func sobekValueExists(v sobek.Value) bool { + return v != nil && !sobek.IsUndefined(v) && !sobek.IsNull(v) } // convert is a helper function to convert any value to a given type. @@ -253,11 +237,11 @@ func convert[T any](from any, to *T) error { } // TODO: -// remove this temporary helper after ensuring the goja-free +// remove this temporary helper after ensuring the sobek-free // business logic works. -func escapesGojaValues(args ...any) bool { +func escapesSobekValues(args ...any) bool { for _, arg := range args { - if _, ok := arg.(goja.Value); ok { + if _, ok := arg.(sobek.Value); ok { return true } } diff --git a/vendor/github.com/grafana/xk6-browser/common/http.go b/vendor/github.com/grafana/xk6-browser/common/http.go index 20f5b765523..df7a1e509f9 100644 --- a/vendor/github.com/grafana/xk6-browser/common/http.go +++ b/vendor/github.com/grafana/xk6-browser/common/http.go @@ -12,7 +12,7 @@ import ( "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/network" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/k6ext" "github.com/grafana/xk6-browser/log" @@ -179,12 +179,12 @@ func (r *Request) Frame() *Frame { } // HeaderValue returns the value of the given header. -func (r *Request) HeaderValue(name string) goja.Value { +func (r *Request) HeaderValue(name string) sobek.Value { rt := r.vu.Runtime() headers := r.AllHeaders() - val, ok := headers[name] + val, ok := headers[strings.ToLower(name)] if !ok { - return goja.Null() + return sobek.Null() } return rt.ToValue(val) } @@ -225,9 +225,8 @@ func (r *Request) PostData() string { } // PostDataBuffer returns the request post data as an ArrayBuffer. -func (r *Request) PostDataBuffer() goja.ArrayBuffer { - rt := r.vu.Runtime() - return rt.NewArrayBuffer([]byte(r.postData)) +func (r *Request) PostDataBuffer() []byte { + return []byte(r.postData) } // ResourceType returns the request resource type. @@ -249,7 +248,7 @@ func (r *Request) Size() HTTPMessageSize { } // Timing returns the request timing information. -func (r *Request) Timing() goja.Value { +func (r *Request) Timing() sobek.Value { type resourceTiming struct { StartTime float64 `js:"startTime"` DomainLookupStart float64 `js:"domainLookupStart"` @@ -419,18 +418,19 @@ func (r *Response) AllHeaders() map[string]string { return headers } -// Body returns the response body as a binary buffer. -func (r *Response) Body() goja.ArrayBuffer { +// Body returns the response body as a bytes buffer. +func (r *Response) Body() ([]byte, error) { if r.status >= 300 && r.status <= 399 { - k6ext.Panic(r.ctx, "Response body is unavailable for redirect responses") + return nil, fmt.Errorf("response body is unavailable for redirect responses") } if err := r.fetchBody(); err != nil { - k6ext.Panic(r.ctx, "getting response body: %w", err) + return nil, fmt.Errorf("getting response body: %w", err) } + r.bodyMu.RLock() defer r.bodyMu.RUnlock() - rt := r.vu.Runtime() - return rt.NewArrayBuffer(r.body) + + return r.body, nil } // bodySize returns the size in bytes of the response body. @@ -457,14 +457,11 @@ func (r *Response) Frame() *Frame { } // HeaderValue returns the value of the given header. -func (r *Response) HeaderValue(name string) goja.Value { +// Returns true if the header is present, false otherwise. +func (r *Response) HeaderValue(name string) (string, bool) { headers := r.AllHeaders() - val, ok := headers[name] - if !ok { - return goja.Null() - } - rt := r.vu.Runtime() - return rt.ToValue(val) + v, ok := headers[strings.ToLower(name)] + return v, ok } // HeaderValues returns the values of the given header. @@ -509,23 +506,24 @@ func (r *Response) HeadersArray() []HTTPHeader { } // JSON returns the response body as JSON data. -func (r *Response) JSON() goja.Value { - if r.cachedJSON == nil { - if err := r.fetchBody(); err != nil { - k6ext.Panic(r.ctx, "getting response body: %w", err) - } +func (r *Response) JSON() (any, error) { + if r.cachedJSON != nil { + return r.cachedJSON, nil + } + if err := r.fetchBody(); err != nil { + return nil, fmt.Errorf("getting response body: %w", err) + } - var v any - r.bodyMu.RLock() - defer r.bodyMu.RUnlock() - if err := json.Unmarshal(r.body, &v); err != nil { - k6ext.Panic(r.ctx, "unmarshalling response body to JSON: %w", err) - } - r.cachedJSON = v + r.bodyMu.RLock() + defer r.bodyMu.RUnlock() + + var v any + if err := json.Unmarshal(r.body, &v); err != nil { + return nil, fmt.Errorf("unmarshalling response body to JSON: %w", err) } - rt := r.vu.Runtime() + r.cachedJSON = v - return rt.ToValue(r.cachedJSON) + return v, nil } // Ok returns true if status code of response if considered ok, otherwise returns false. @@ -542,15 +540,13 @@ func (r *Response) Request() *Request { } // SecurityDetails returns the security details of the response. -func (r *Response) SecurityDetails() goja.Value { - rt := r.vu.Runtime() - return rt.ToValue(r.securityDetails) +func (r *Response) SecurityDetails() *SecurityDetails { + return r.securityDetails } // ServerAddr returns the remote address of the server. -func (r *Response) ServerAddr() goja.Value { - rt := r.vu.Runtime() - return rt.ToValue(r.remoteAddress) +func (r *Response) ServerAddr() *RemoteAddress { + return r.remoteAddress } // Size returns the size in bytes of the response. @@ -572,13 +568,15 @@ func (r *Response) StatusText() string { } // Text returns the response body as a string. -func (r *Response) Text() string { +func (r *Response) Text() (string, error) { if err := r.fetchBody(); err != nil { - k6ext.Panic(r.ctx, "getting response body as text: %w", err) + return "", fmt.Errorf("getting response body as text: %w", err) } + r.bodyMu.RLock() defer r.bodyMu.RUnlock() - return string(r.body) + + return string(r.body), nil } // URL returns the request URL. diff --git a/vendor/github.com/grafana/xk6-browser/common/js_handle.go b/vendor/github.com/grafana/xk6-browser/common/js_handle.go index f52469e24a6..3f17b79acc8 100644 --- a/vendor/github.com/grafana/xk6-browser/common/js_handle.go +++ b/vendor/github.com/grafana/xk6-browser/common/js_handle.go @@ -6,7 +6,6 @@ import ( "fmt" "strings" - "github.com/grafana/xk6-browser/k6ext" "github.com/grafana/xk6-browser/log" "github.com/chromedp/cdproto/cdp" @@ -21,8 +20,8 @@ import ( // JSHandleAPI interface. type JSHandleAPI interface { AsElement() *ElementHandle - Dispose() - Evaluate(pageFunc string, args ...any) any + Dispose() error + Evaluate(pageFunc string, args ...any) (any, error) EvaluateHandle(pageFunc string, args ...any) (JSHandleAPI, error) GetProperties() (map[string]JSHandleAPI, error) JSONValue() (string, error) @@ -79,30 +78,33 @@ func (h *BaseJSHandle) AsElement() *ElementHandle { } // Dispose releases the remote object. -func (h *BaseJSHandle) Dispose() { - if err := h.dispose(); err != nil { - // We do not want to panic on an error when the error is a closed - // context. The reason the context would be closed is due to the - // iteration ending and therefore the associated browser and its assets - // will be automatically deleted. - if errors.Is(err, context.Canceled) { - h.logger.Debugf("BaseJSHandle:Dispose", "%v", err) - return - } - // The following error indicates that the object we're trying to release - // cannot be found, which would mean that the object has already been - // removed/deleted. This can occur when a navigation occurs, usually when - // a page contains an iframe. - if strings.Contains(err.Error(), "Cannot find context with specified id") { - h.logger.Debugf("BaseJSHandle:Dispose", "%v", err) - return - } +func (h *BaseJSHandle) Dispose() error { + err := h.dispose() + if err == nil { // no error + return nil + } - k6ext.Panic(h.ctx, "dispose: %w", err) + // We do not want to return an error when the error is a closed + // context. The reason the context would be closed is due to the + // iteration ending and therefore the associated browser and its assets + // will be automatically deleted. + if errors.Is(err, context.Canceled) { + h.logger.Debugf("BaseJSHandle:Dispose", "%v", err) + return nil } + // The following error indicates that the object we're trying to release + // cannot be found, which would mean that the object has already been + // removed/deleted. This can occur when a navigation occurs, usually when + // a page contains an iframe. + if strings.Contains(err.Error(), "Cannot find context with specified id") { + h.logger.Debugf("BaseJSHandle:Dispose", "%v", err) + return nil + } + + return fmt.Errorf("disposing element with ID %s: %w", h.remoteObject.ObjectID, err) } -// dispose is like Dispose, but does not panic. +// dispose sends a command to the browser to release the remote object. func (h *BaseJSHandle) dispose() error { if h.disposed { return nil @@ -121,12 +123,13 @@ func (h *BaseJSHandle) dispose() error { } // Evaluate will evaluate provided page function within an execution context. -func (h *BaseJSHandle) Evaluate(pageFunc string, args ...any) any { +func (h *BaseJSHandle) Evaluate(pageFunc string, args ...any) (any, error) { res, err := h.execCtx.Eval(h.ctx, pageFunc, args...) if err != nil { - k6ext.Panic(h.ctx, "%w", err) + return nil, fmt.Errorf("evaluating element: %w", err) } - return res + + return res, nil } // EvaluateHandle will evaluate provided page function within an execution context. diff --git a/vendor/github.com/grafana/xk6-browser/common/keyboard.go b/vendor/github.com/grafana/xk6-browser/common/keyboard.go index 50735de582b..e8007c0f772 100644 --- a/vendor/github.com/grafana/xk6-browser/common/keyboard.go +++ b/vendor/github.com/grafana/xk6-browser/common/keyboard.go @@ -6,12 +6,11 @@ import ( "strings" "time" - "github.com/grafana/xk6-browser/k6ext" - "github.com/grafana/xk6-browser/keyboardlayout" - "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/input" - "github.com/dop251/goja" + "github.com/grafana/sobek" + + "github.com/grafana/xk6-browser/keyboardlayout" ) const ( @@ -45,38 +44,43 @@ func NewKeyboard(ctx context.Context, s session) *Keyboard { } // Down sends a key down message to a session target. -func (k *Keyboard) Down(key string) { +func (k *Keyboard) Down(key string) error { if err := k.down(key); err != nil { - k6ext.Panic(k.ctx, "sending key down: %w", err) + return fmt.Errorf("sending key down: %w", err) } + return nil } // Up sends a key up message to a session target. -func (k *Keyboard) Up(key string) { +func (k *Keyboard) Up(key string) error { if err := k.up(key); err != nil { - k6ext.Panic(k.ctx, "sending key up: %w", err) + return fmt.Errorf("sending key up: %w", err) } + return nil } // Press sends a key press message to a session target. // It delays the action if `Delay` option is specified. // A press message consists of successive key down and up messages. -func (k *Keyboard) Press(key string, opts goja.Value) { +func (k *Keyboard) Press(key string, opts sobek.Value) error { kbdOpts := NewKeyboardOptions() if err := kbdOpts.Parse(k.ctx, opts); err != nil { - k6ext.Panic(k.ctx, "parsing keyboard options: %w", err) + return fmt.Errorf("parsing keyboard options: %w", err) } if err := k.comboPress(key, kbdOpts); err != nil { - k6ext.Panic(k.ctx, "pressing key: %w", err) + return fmt.Errorf("pressing key: %w", err) } + + return nil } // InsertText inserts a text without dispatching key events. -func (k *Keyboard) InsertText(text string) { +func (k *Keyboard) InsertText(text string) error { if err := k.insertText(text); err != nil { - k6ext.Panic(k.ctx, "inserting text: %w", err) + return fmt.Errorf("inserting text: %w", err) } + return nil } // Type sends a press message to a session target for each character in text. @@ -84,14 +88,15 @@ func (k *Keyboard) InsertText(text string) { // // It sends an insertText message if a character is not among // valid characters in the keyboard's layout. -func (k *Keyboard) Type(text string, opts goja.Value) { +func (k *Keyboard) Type(text string, opts sobek.Value) error { kbdOpts := NewKeyboardOptions() if err := kbdOpts.Parse(k.ctx, opts); err != nil { - k6ext.Panic(k.ctx, "parsing keyboard options: %w", err) + return fmt.Errorf("parsing keyboard options: %w", err) } if err := k.typ(text, kbdOpts); err != nil { - k6ext.Panic(k.ctx, "typing text: %w", err) + return fmt.Errorf("typing text: %w", err) } + return nil } func (k *Keyboard) down(key string) error { diff --git a/vendor/github.com/grafana/xk6-browser/common/keyboard_options.go b/vendor/github.com/grafana/xk6-browser/common/keyboard_options.go index da39f46c8b2..c10376ac535 100644 --- a/vendor/github.com/grafana/xk6-browser/common/keyboard_options.go +++ b/vendor/github.com/grafana/xk6-browser/common/keyboard_options.go @@ -3,7 +3,7 @@ package common import ( "context" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/k6ext" ) @@ -18,9 +18,10 @@ func NewKeyboardOptions() *KeyboardOptions { } } -func (o *KeyboardOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the keyboard options. +func (o *KeyboardOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { diff --git a/vendor/github.com/grafana/xk6-browser/common/layout.go b/vendor/github.com/grafana/xk6-browser/common/layout.go index ade023d3b20..afc8ad96476 100644 --- a/vendor/github.com/grafana/xk6-browser/common/layout.go +++ b/vendor/github.com/grafana/xk6-browser/common/layout.go @@ -5,7 +5,7 @@ import ( "fmt" "math" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/k6ext" ) @@ -52,10 +52,10 @@ func (s Size) enclosingIntSize() *Size { } } -// Parse size details from a given goja viewport value. -func (s *Size) Parse(ctx context.Context, viewport goja.Value) error { +// Parse size details from a given sobek viewport value. +func (s *Size) Parse(ctx context.Context, viewport sobek.Value) error { rt := k6ext.Runtime(ctx) - if viewport != nil && !goja.IsUndefined(viewport) && !goja.IsNull(viewport) { + if viewport != nil && !sobek.IsUndefined(viewport) && !sobek.IsNull(viewport) { viewport := viewport.ToObject(rt) for _, k := range viewport.Keys() { switch k { @@ -80,10 +80,10 @@ type Viewport struct { Height int64 `js:"height"` } -// Parse viewport details from a given goja viewport value. -func (v *Viewport) Parse(ctx context.Context, viewport goja.Value) error { +// Parse viewport details from a given sobek viewport value. +func (v *Viewport) Parse(ctx context.Context, viewport sobek.Value) error { rt := k6ext.Runtime(ctx) - if viewport != nil && !goja.IsUndefined(viewport) && !goja.IsNull(viewport) { + if viewport != nil && !sobek.IsUndefined(viewport) && !sobek.IsNull(viewport) { viewport := viewport.ToObject(rt) for _, k := range viewport.Keys() { switch k { diff --git a/vendor/github.com/grafana/xk6-browser/common/locator.go b/vendor/github.com/grafana/xk6-browser/common/locator.go index acc1b6fddfe..89158ae62e9 100644 --- a/vendor/github.com/grafana/xk6-browser/common/locator.go +++ b/vendor/github.com/grafana/xk6-browser/common/locator.go @@ -5,10 +5,9 @@ import ( "fmt" "time" - "github.com/grafana/xk6-browser/k6ext" - "github.com/grafana/xk6-browser/log" + "github.com/grafana/sobek" - "github.com/dop251/goja" + "github.com/grafana/xk6-browser/log" ) // Strict mode: @@ -82,21 +81,20 @@ func (l *Locator) click(opts *FrameClickOptions) error { } // Dblclick double clicks on an element using locator's selector with strict mode on. -func (l *Locator) Dblclick(opts goja.Value) { +func (l *Locator) Dblclick(opts sobek.Value) error { l.log.Debugf("Locator:Dblclick", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameDblClickOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing double click options: %w", err) - return + if err := copts.Parse(l.ctx, opts); err != nil { + return fmt.Errorf("parsing double click options: %w", err) } - if err = l.dblclick(copts); err != nil { - err = fmt.Errorf("double clicking on %q: %w", l.selector, err) - return + if err := l.dblclick(copts); err != nil { + return fmt.Errorf("double clicking on %q: %w", l.selector, err) } + + applySlowMo(l.ctx) + + return nil } // Dblclick is like Dblclick but takes parsed options and neither throws an @@ -107,21 +105,20 @@ func (l *Locator) dblclick(opts *FrameDblclickOptions) error { } // Check on an element using locator's selector with strict mode on. -func (l *Locator) Check(opts goja.Value) { +func (l *Locator) Check(opts sobek.Value) error { l.log.Debugf("Locator:Check", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameCheckOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing check options: %w", err) - return + if err := copts.Parse(l.ctx, opts); err != nil { + return fmt.Errorf("parsing check options: %w", err) } - if err = l.check(copts); err != nil { - err = fmt.Errorf("checking %q: %w", l.selector, err) - return + if err := l.check(copts); err != nil { + return fmt.Errorf("checking %q: %w", l.selector, err) } + + applySlowMo(l.ctx) + + return nil } // check is like Check but takes parsed options and neither throws an @@ -132,21 +129,20 @@ func (l *Locator) check(opts *FrameCheckOptions) error { } // Uncheck on an element using locator's selector with strict mode on. -func (l *Locator) Uncheck(opts goja.Value) { +func (l *Locator) Uncheck(opts sobek.Value) error { l.log.Debugf("Locator:Uncheck", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameUncheckOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing uncheck options: %w", err) - return + if err := copts.Parse(l.ctx, opts); err != nil { + return fmt.Errorf("parsing uncheck options: %w", err) } - if err = l.uncheck(copts); err != nil { - err = fmt.Errorf("unchecking %q: %w", l.selector, err) - return + if err := l.uncheck(copts); err != nil { + return fmt.Errorf("unchecking %q: %w", l.selector, err) } + + applySlowMo(l.ctx) + + return nil } // uncheck is like Uncheck but takes parsed options and neither throws @@ -158,19 +154,19 @@ func (l *Locator) uncheck(opts *FrameUncheckOptions) error { // IsChecked returns true if the element matches the locator's // selector and is checked. Otherwise, returns false. -func (l *Locator) IsChecked(opts goja.Value) bool { +func (l *Locator) IsChecked(opts sobek.Value) (bool, error) { l.log.Debugf("Locator:IsChecked", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) copts := NewFrameIsCheckedOptions(l.frame.defaultTimeout()) if err := copts.Parse(l.ctx, opts); err != nil { - k6ext.Panic(l.ctx, "parsing is checked options: %w", err) + return false, fmt.Errorf("parsing is checked options: %w", err) } checked, err := l.isChecked(copts) if err != nil { - k6ext.Panic(l.ctx, "checking is %q checked: %w", l.selector, err) + return false, fmt.Errorf("checking is %q checked: %w", l.selector, err) } - return checked + return checked, nil } // isChecked is like IsChecked but takes parsed options and does not @@ -182,19 +178,19 @@ func (l *Locator) isChecked(opts *FrameIsCheckedOptions) (bool, error) { // IsEditable returns true if the element matches the locator's // selector and is Editable. Otherwise, returns false. -func (l *Locator) IsEditable(opts goja.Value) bool { +func (l *Locator) IsEditable(opts sobek.Value) (bool, error) { l.log.Debugf("Locator:IsEditable", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) copts := NewFrameIsEditableOptions(l.frame.defaultTimeout()) if err := copts.Parse(l.ctx, opts); err != nil { - k6ext.Panic(l.ctx, "parsing is editable options: %w", err) + return false, fmt.Errorf("parsing is editable options: %w", err) } editable, err := l.isEditable(copts) if err != nil { - k6ext.Panic(l.ctx, "checking is %q editable: %w", l.selector, err) + return false, fmt.Errorf("checking is %q editable: %w", l.selector, err) } - return editable + return editable, nil } // isEditable is like IsEditable but takes parsed options and does not @@ -206,19 +202,19 @@ func (l *Locator) isEditable(opts *FrameIsEditableOptions) (bool, error) { // IsEnabled returns true if the element matches the locator's // selector and is Enabled. Otherwise, returns false. -func (l *Locator) IsEnabled(opts goja.Value) bool { +func (l *Locator) IsEnabled(opts sobek.Value) (bool, error) { l.log.Debugf("Locator:IsEnabled", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) copts := NewFrameIsEnabledOptions(l.frame.defaultTimeout()) if err := copts.Parse(l.ctx, opts); err != nil { - k6ext.Panic(l.ctx, "parsing is enabled options: %w", err) + return false, fmt.Errorf("parsing is enabled options: %w", err) } enabled, err := l.isEnabled(copts) if err != nil { - k6ext.Panic(l.ctx, "checking is %q enabled: %w", l.selector, err) + return false, fmt.Errorf("checking is %q enabled: %w", l.selector, err) } - return enabled + return enabled, nil } // isEnabled is like IsEnabled but takes parsed options and does not @@ -230,19 +226,19 @@ func (l *Locator) isEnabled(opts *FrameIsEnabledOptions) (bool, error) { // IsDisabled returns true if the element matches the locator's // selector and is disabled. Otherwise, returns false. -func (l *Locator) IsDisabled(opts goja.Value) bool { +func (l *Locator) IsDisabled(opts sobek.Value) (bool, error) { l.log.Debugf("Locator:IsDisabled", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) copts := NewFrameIsDisabledOptions(l.frame.defaultTimeout()) if err := copts.Parse(l.ctx, opts); err != nil { - k6ext.Panic(l.ctx, "parsing is disabled options: %w", err) + return false, fmt.Errorf("parsing is disabled options: %w", err) } disabled, err := l.isDisabled(copts) if err != nil { - k6ext.Panic(l.ctx, "checking is %q disabled: %w", l.selector, err) + return false, fmt.Errorf("checking is %q disabled: %w", l.selector, err) } - return disabled + return disabled, nil } // IsDisabled is like IsDisabled but takes parsed options and does not @@ -279,24 +275,23 @@ func (l *Locator) IsHidden() (bool, error) { } // Fill out the element using locator's selector with strict mode on. -func (l *Locator) Fill(value string, opts goja.Value) { +func (l *Locator) Fill(value string, opts sobek.Value) error { l.log.Debugf( "Locator:Fill", "fid:%s furl:%q sel:%q val:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, value, opts, ) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameFillOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing fill options: %w", err) - return + if err := copts.Parse(l.ctx, opts); err != nil { + return fmt.Errorf("parsing fill options: %w", err) } - if err = l.fill(value, copts); err != nil { - err = fmt.Errorf("filling %q with %q: %w", l.selector, value, err) - return + if err := l.fill(value, copts); err != nil { + return fmt.Errorf("filling %q with %q: %w", l.selector, value, err) } + + applySlowMo(l.ctx) + + return nil } func (l *Locator) fill(value string, opts *FrameFillOptions) error { @@ -305,21 +300,20 @@ func (l *Locator) fill(value string, opts *FrameFillOptions) error { } // Focus on the element using locator's selector with strict mode on. -func (l *Locator) Focus(opts goja.Value) { +func (l *Locator) Focus(opts sobek.Value) error { l.log.Debugf("Locator:Focus", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameBaseOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing focus options: %w", err) - return + if err := copts.Parse(l.ctx, opts); err != nil { + return fmt.Errorf("parsing focus options: %w", err) } - if err = l.focus(copts); err != nil { - err = fmt.Errorf("focusing on %q: %w", l.selector, err) - return + if err := l.focus(copts); err != nil { + return fmt.Errorf("focusing on %q: %w", l.selector, err) } + + applySlowMo(l.ctx) + + return nil } func (l *Locator) focus(opts *FrameBaseOptions) error { @@ -328,54 +322,45 @@ func (l *Locator) focus(opts *FrameBaseOptions) error { } // GetAttribute of the element using locator's selector with strict mode on. -func (l *Locator) GetAttribute(name string, opts goja.Value) any { +// The second return value is true if the attribute exists, and false otherwise. +func (l *Locator) GetAttribute(name string, opts sobek.Value) (string, bool, error) { l.log.Debugf( "Locator:GetAttribute", "fid:%s furl:%q sel:%q name:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, name, opts, ) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameBaseOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing get attribute options: %w", err) - return nil + if err := copts.Parse(l.ctx, opts); err != nil { + return "", false, fmt.Errorf("parsing get attribute options: %w", err) } - var v any - if v, err = l.getAttribute(name, copts); err != nil { - err = fmt.Errorf("getting attribute %q of %q: %w", name, l.selector, err) - return nil + s, ok, err := l.getAttribute(name, copts) + if err != nil { + return "", false, fmt.Errorf("getting attribute %q of %q: %w", name, l.selector, err) } - return v + return s, ok, nil } -func (l *Locator) getAttribute(name string, opts *FrameBaseOptions) (any, error) { +func (l *Locator) getAttribute(name string, opts *FrameBaseOptions) (string, bool, error) { opts.Strict = true return l.frame.getAttribute(l.selector, name, opts) } // InnerHTML returns the element's inner HTML that matches // the locator's selector with strict mode on. -func (l *Locator) InnerHTML(opts goja.Value) string { +func (l *Locator) InnerHTML(opts sobek.Value) (string, error) { l.log.Debugf("Locator:InnerHTML", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameInnerHTMLOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing inner HTML options: %w", err) - return "" + if err := copts.Parse(l.ctx, opts); err != nil { + return "", fmt.Errorf("parsing inner HTML options: %w", err) } - var s string - if s, err = l.innerHTML(copts); err != nil { - err = fmt.Errorf("getting inner HTML of %q: %w", l.selector, err) - return "" + s, err := l.innerHTML(copts) + if err != nil { + return "", fmt.Errorf("getting inner HTML of %q: %w", l.selector, err) } - return s + return s, nil } func (l *Locator) innerHTML(opts *FrameInnerHTMLOptions) (string, error) { @@ -385,24 +370,19 @@ func (l *Locator) innerHTML(opts *FrameInnerHTMLOptions) (string, error) { // InnerText returns the element's inner text that matches // the locator's selector with strict mode on. -func (l *Locator) InnerText(opts goja.Value) string { +func (l *Locator) InnerText(opts sobek.Value) (string, error) { l.log.Debugf("Locator:InnerText", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameInnerTextOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing inner text options: %w", err) - return "" + if err := copts.Parse(l.ctx, opts); err != nil { + return "", fmt.Errorf("parsing inner text options: %w", err) } - var s string - if s, err = l.innerText(copts); err != nil { - err = fmt.Errorf("getting inner text of %q: %w", l.selector, err) - return "" + s, err := l.innerText(copts) + if err != nil { + return "", fmt.Errorf("getting inner text of %q: %w", l.selector, err) } - return s + return s, nil } func (l *Locator) innerText(opts *FrameInnerTextOptions) (string, error) { @@ -411,47 +391,44 @@ func (l *Locator) innerText(opts *FrameInnerTextOptions) (string, error) { } // TextContent returns the element's text content that matches -// the locator's selector with strict mode on. -func (l *Locator) TextContent(opts goja.Value) string { +// the locator's selector with strict mode on. The second return +// value is true if the returned text content is not null or empty, +// and false otherwise. +func (l *Locator) TextContent(opts sobek.Value) (string, bool, error) { l.log.Debugf("Locator:TextContent", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameTextContentOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing text context options: %w", err) - return "" + if err := copts.Parse(l.ctx, opts); err != nil { + return "", false, fmt.Errorf("parsing text context options: %w", err) } - var s string - if s, err = l.textContent(copts); err != nil { - err = fmt.Errorf("getting text content of %q: %w", l.selector, err) - return "" + s, ok, err := l.textContent(copts) + if err != nil { + return "", false, fmt.Errorf("getting text content of %q: %w", l.selector, err) } - return s + return s, ok, nil } -func (l *Locator) textContent(opts *FrameTextContentOptions) (string, error) { +func (l *Locator) textContent(opts *FrameTextContentOptions) (string, bool, error) { opts.Strict = true return l.frame.textContent(l.selector, opts) } // InputValue returns the element's input value that matches // the locator's selector with strict mode on. -func (l *Locator) InputValue(opts goja.Value) string { +func (l *Locator) InputValue(opts sobek.Value) (string, error) { l.log.Debugf("Locator:InputValue", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) copts := NewFrameInputValueOptions(l.frame.defaultTimeout()) if err := copts.Parse(l.ctx, opts); err != nil { - k6ext.Panic(l.ctx, "parsing input value options: %w", err) + return "", fmt.Errorf("parsing input value options: %w", err) } v, err := l.inputValue(copts) if err != nil { - k6ext.Panic(l.ctx, "getting input value of %q: %w", l.selector, err) + return "", fmt.Errorf("getting input value of %q: %w", l.selector, err) } - return v + return v, nil } func (l *Locator) inputValue(opts *FrameInputValueOptions) (string, error) { @@ -462,46 +439,47 @@ func (l *Locator) inputValue(opts *FrameInputValueOptions) (string, error) { // SelectOption filters option values of the first element that matches // the locator's selector (with strict mode on), selects the options, // and returns the filtered options. -func (l *Locator) SelectOption(values goja.Value, opts goja.Value) []string { +func (l *Locator) SelectOption(values sobek.Value, opts sobek.Value) ([]string, error) { l.log.Debugf("Locator:SelectOption", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) copts := NewFrameSelectOptionOptions(l.frame.defaultTimeout()) if err := copts.Parse(l.ctx, opts); err != nil { - k6ext.Panic(l.ctx, "parsing select option options: %w", err) + return nil, fmt.Errorf("parsing select option options: %w", err) } v, err := l.selectOption(values, copts) if err != nil { - k6ext.Panic(l.ctx, "selecting option on %q: %w", l.selector, err) + return nil, fmt.Errorf("selecting option on %q: %w", l.selector, err) } - return v + applySlowMo(l.ctx) + + return v, nil } -func (l *Locator) selectOption(values goja.Value, opts *FrameSelectOptionOptions) ([]string, error) { +func (l *Locator) selectOption(values sobek.Value, opts *FrameSelectOptionOptions) ([]string, error) { opts.Strict = true return l.frame.selectOption(l.selector, values, opts) } // Press the given key on the element found that matches the locator's // selector with strict mode on. -func (l *Locator) Press(key string, opts goja.Value) { +func (l *Locator) Press(key string, opts sobek.Value) error { l.log.Debugf( "Locator:Press", "fid:%s furl:%q sel:%q key:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, key, opts, ) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFramePressOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing press options: %w", err) - return + if err := copts.Parse(l.ctx, opts); err != nil { + return fmt.Errorf("parsing press options: %w", err) } - if err = l.press(key, copts); err != nil { - err = fmt.Errorf("pressing %q on %q: %w", key, l.selector, err) - return + if err := l.press(key, copts); err != nil { + return fmt.Errorf("pressing %q on %q: %w", key, l.selector, err) } + + applySlowMo(l.ctx) + + return nil } func (l *Locator) press(key string, opts *FramePressOptions) error { @@ -511,7 +489,7 @@ func (l *Locator) press(key string, opts *FramePressOptions) error { // Type text on the element found that matches the locator's // selector with strict mode on. -func (l *Locator) Type(text string, opts goja.Value) error { +func (l *Locator) Type(text string, opts sobek.Value) error { l.log.Debugf( "Locator:Type", "fid:%s furl:%q sel:%q text:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, text, opts, @@ -543,21 +521,20 @@ func (l *Locator) typ(text string, opts *FrameTypeOptions) error { // Hover moves the pointer over the element that matches the locator's // selector with strict mode on. -func (l *Locator) Hover(opts goja.Value) { +func (l *Locator) Hover(opts sobek.Value) error { l.log.Debugf("Locator:Hover", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - copts := NewFrameHoverOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing hover options: %w", err) - return + if err := copts.Parse(l.ctx, opts); err != nil { + return fmt.Errorf("parsing hover options: %w", err) } - if err = l.hover(copts); err != nil { - err = fmt.Errorf("hovering on %q: %w", l.selector, err) - return + if err := l.hover(copts); err != nil { + return fmt.Errorf("hovering on %q: %w", l.selector, err) } + + applySlowMo(l.ctx) + + return nil } func (l *Locator) hover(opts *FrameHoverOptions) error { @@ -587,13 +564,12 @@ func (l *Locator) DispatchEvent(typ string, eventInit any, opts *FrameDispatchEv l.frame.ID(), l.frame.URL(), l.selector, typ, eventInit, opts, ) - var err error - defer func() { panicOrSlowMo(l.ctx, err) }() - - if err = l.dispatchEvent(typ, eventInit, opts); err != nil { + if err := l.dispatchEvent(typ, eventInit, opts); err != nil { return fmt.Errorf("dispatching locator event %q to %q: %w", typ, l.selector, err) } + applySlowMo(l.ctx) + return nil } @@ -603,16 +579,18 @@ func (l *Locator) dispatchEvent(typ string, eventInit any, opts *FrameDispatchEv } // WaitFor waits for the element matching the locator's selector with strict mode on. -func (l *Locator) WaitFor(opts goja.Value) { +func (l *Locator) WaitFor(opts sobek.Value) error { l.log.Debugf("Locator:WaitFor", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts) popts := NewFrameWaitForSelectorOptions(l.frame.defaultTimeout()) if err := popts.Parse(l.ctx, opts); err != nil { - k6ext.Panic(l.ctx, "parsing wait for options: %w", err) + return fmt.Errorf("parsing wait for options: %w", err) } if err := l.waitFor(popts); err != nil { - k6ext.Panic(l.ctx, "waiting for %q: %w", l.selector, err) + return fmt.Errorf("waiting for %q: %w", l.selector, err) } + + return nil } func (l *Locator) waitFor(opts *FrameWaitForSelectorOptions) error { diff --git a/vendor/github.com/grafana/xk6-browser/common/mouse.go b/vendor/github.com/grafana/xk6-browser/common/mouse.go index afab7aba7db..0df78e6f455 100644 --- a/vendor/github.com/grafana/xk6-browser/common/mouse.go +++ b/vendor/github.com/grafana/xk6-browser/common/mouse.go @@ -2,13 +2,12 @@ package common import ( "context" + "fmt" "time" - "github.com/grafana/xk6-browser/k6ext" - "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/input" - "github.com/dop251/goja" + "github.com/grafana/sobek" ) // Mouse represents a mouse input device. @@ -35,13 +34,25 @@ func NewMouse(ctx context.Context, s session, f *Frame, ts *TimeoutSettings, k * } } +// Click will trigger a series of MouseMove, MouseDown and MouseUp events in the browser. +func (m *Mouse) Click(x float64, y float64, opts sobek.Value) error { + mouseOpts := NewMouseClickOptions() + if err := mouseOpts.Parse(m.ctx, opts); err != nil { + return fmt.Errorf("parsing mouse click options: %w", err) + } + if err := m.click(x, y, mouseOpts); err != nil { + return fmt.Errorf("clicking on x:%f y:%f: %w", x, y, err) + } + return nil +} + func (m *Mouse) click(x float64, y float64, opts *MouseClickOptions) error { mouseDownUpOpts := opts.ToMouseDownUpOptions() if err := m.move(x, y, NewMouseMoveOptions()); err != nil { return err } for i := 0; i < int(mouseDownUpOpts.ClickCount); i++ { - if err := m.down(x, y, mouseDownUpOpts); err != nil { + if err := m.down(mouseDownUpOpts); err != nil { return err } if opts.Delay != 0 { @@ -52,131 +63,102 @@ func (m *Mouse) click(x float64, y float64, opts *MouseClickOptions) error { case <-t.C: } } - if err := m.up(x, y, mouseDownUpOpts); err != nil { + if err := m.up(mouseDownUpOpts); err != nil { return err } } + return nil } -func (m *Mouse) down(x float64, y float64, opts *MouseDownUpOptions) error { - m.button = input.MouseButton(opts.Button) - action := input.DispatchMouseEvent(input.MousePressed, m.x, m.y). - WithButton(input.MouseButton(opts.Button)). - WithModifiers(input.Modifier(m.keyboard.modifiers)). - WithClickCount(opts.ClickCount) - if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { - return err +// DblClick will trigger Click twice in quick succession. +func (m *Mouse) DblClick(x float64, y float64, opts sobek.Value) error { + mouseOpts := NewMouseDblClickOptions() + if err := mouseOpts.Parse(m.ctx, opts); err != nil { + return fmt.Errorf("parsing double click options: %w", err) + } + if err := m.click(x, y, mouseOpts.ToMouseClickOptions()); err != nil { + return fmt.Errorf("double clicking on x:%f y:%f: %w", x, y, err) } return nil } -func (m *Mouse) move(x float64, y float64, opts *MouseMoveOptions) error { - var fromX float64 = m.x - var fromY float64 = m.y - m.x = x - m.y = y - for i := int64(1); i <= opts.Steps; i++ { - x := fromX + (m.x-fromX)*float64(i/opts.Steps) - y := fromY + (m.y-fromY)*float64(i/opts.Steps) - action := input.DispatchMouseEvent(input.MouseMoved, x, y). - WithButton(m.button). - WithModifiers(input.Modifier(m.keyboard.modifiers)) - if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { - return err - } +// Down will trigger a MouseDown event in the browser. +func (m *Mouse) Down(opts sobek.Value) error { + mouseOpts := NewMouseDownUpOptions() + if err := mouseOpts.Parse(m.ctx, opts); err != nil { + return fmt.Errorf("parsing mouse down options: %w", err) + } + if err := m.down(mouseOpts); err != nil { + return fmt.Errorf("pressing the mouse button on x:%f y:%f: %w", m.x, m.y, err) } return nil } -func (m *Mouse) up(x float64, y float64, opts *MouseDownUpOptions) error { - m.button = input.None - action := input.DispatchMouseEvent(input.MouseReleased, m.x, m.y). +func (m *Mouse) down(opts *MouseDownUpOptions) error { + m.button = input.MouseButton(opts.Button) + action := input.DispatchMouseEvent(input.MousePressed, m.x, m.y). WithButton(input.MouseButton(opts.Button)). WithModifiers(input.Modifier(m.keyboard.modifiers)). WithClickCount(opts.ClickCount) if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { - return err + return fmt.Errorf("mouse down: %w", err) } return nil } -// Click will trigger a series of MouseMove, MouseDown and MouseUp events in the browser. -func (m *Mouse) Click(x float64, y float64, opts goja.Value) { - mouseOpts := NewMouseClickOptions() +// Up will trigger a MouseUp event in the browser. +func (m *Mouse) Up(opts sobek.Value) error { + mouseOpts := NewMouseDownUpOptions() if err := mouseOpts.Parse(m.ctx, opts); err != nil { - k6ext.Panic(m.ctx, "parsing mouse click options: %w", err) + return fmt.Errorf("parsing mouse up options: %w", err) } - if err := m.click(x, y, mouseOpts); err != nil { - k6ext.Panic(m.ctx, "clicking on x:%f y:%f: %w", x, y, err) + if err := m.up(mouseOpts); err != nil { + return fmt.Errorf("releasing the mouse button on x:%f y:%f: %w", m.x, m.y, err) } + return nil } -func (m *Mouse) DblClick(x float64, y float64, opts goja.Value) { - mouseOpts := NewMouseDblClickOptions() - if err := mouseOpts.Parse(m.ctx, opts); err != nil { - k6ext.Panic(m.ctx, "parsing double click options: %w", err) - } - if err := m.click(x, y, mouseOpts.ToMouseClickOptions()); err != nil { - k6ext.Panic(m.ctx, "double clicking on x:%f y:%f: %w", x, y, err) +func (m *Mouse) up(opts *MouseDownUpOptions) error { + m.button = input.None + action := input.DispatchMouseEvent(input.MouseReleased, m.x, m.y). + WithButton(input.MouseButton(opts.Button)). + WithModifiers(input.Modifier(m.keyboard.modifiers)). + WithClickCount(opts.ClickCount) + if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { + return fmt.Errorf("mouse up: %w", err) } -} -// Down will trigger a MouseDown event in the browser. -func (m *Mouse) Down(x float64, y float64, opts goja.Value) { - mouseOpts := NewMouseDownUpOptions() - if err := mouseOpts.Parse(m.ctx, opts); err != nil { - k6ext.Panic(m.ctx, "parsing mouse down options: %w", err) - } - if err := m.down(x, y, mouseOpts); err != nil { - k6ext.Panic(m.ctx, "pressing the mouse button on x:%f y:%f: %w", x, y, err) - } + return nil } // Move will trigger a MouseMoved event in the browser. -func (m *Mouse) Move(x float64, y float64, opts goja.Value) { - mouseOpts := NewMouseDownUpOptions() +func (m *Mouse) Move(x float64, y float64, opts sobek.Value) error { + mouseOpts := NewMouseMoveOptions() if err := mouseOpts.Parse(m.ctx, opts); err != nil { - k6ext.Panic(m.ctx, "parsing mouse move options: %w", err) + return fmt.Errorf("parsing mouse move options: %w", err) } - if err := m.down(x, y, mouseOpts); err != nil { - k6ext.Panic(m.ctx, "moving the mouse pointer to x:%f y:%f: %w", x, y, err) - } -} - -// Up will trigger a MouseUp event in the browser. -func (m *Mouse) Up(x float64, y float64, opts goja.Value) { - mouseOpts := NewMouseDownUpOptions() - if err := mouseOpts.Parse(m.ctx, opts); err != nil { - k6ext.Panic(m.ctx, "parsing mouse up options: %w", err) - } - if err := m.up(x, y, mouseOpts); err != nil { - k6ext.Panic(m.ctx, "releasing the mouse button on x:%f y:%f: %w", x, y, err) + if err := m.move(x, y, mouseOpts); err != nil { + return fmt.Errorf("moving the mouse pointer to x:%f y:%f: %w", x, y, err) } + return nil } -// Wheel will trigger a MouseWheel event in the browser -/*func (m *Mouse) Wheel(opts goja.Value) { - var deltaX float64 = 0.0 - var deltaY float64 = 0.0 - - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { - opts := opts.ToObject(rt) - for _, k := range opts.Keys() { - switch k { - case "deltaX": - deltaX = opts.Get(k).ToFloat() - case "deltaY": - deltaY = opts.Get(k).ToFloat() - } +func (m *Mouse) move(x float64, y float64, opts *MouseMoveOptions) error { + fromX := m.x + fromY := m.y + m.x = x + m.y = y + for i := int64(1); i <= opts.Steps; i++ { + x := fromX + (m.x-fromX)*float64(i/opts.Steps) + y := fromY + (m.y-fromY)*float64(i/opts.Steps) + action := input.DispatchMouseEvent(input.MouseMoved, x, y). + WithButton(m.button). + WithModifiers(input.Modifier(m.keyboard.modifiers)) + if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { + return fmt.Errorf("mouse move: %w", err) } } - action := input.DispatchMouseEvent(input.MouseWheel, m.x, m.y). - WithModifiers(input.Modifier(m.keyboard.modifiers)). - WithDeltaX(deltaX). - WithDeltaY(deltaY) - if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { - k6Throw(m.ctx, "mouse down: %w", err) - } -}*/ + return nil +} diff --git a/vendor/github.com/grafana/xk6-browser/common/mouse_options.go b/vendor/github.com/grafana/xk6-browser/common/mouse_options.go index 7109bfbd657..f2ae7ffb0b1 100644 --- a/vendor/github.com/grafana/xk6-browser/common/mouse_options.go +++ b/vendor/github.com/grafana/xk6-browser/common/mouse_options.go @@ -3,7 +3,7 @@ package common import ( "context" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/k6ext" ) @@ -36,9 +36,10 @@ func NewMouseClickOptions() *MouseClickOptions { } } -func (o *MouseClickOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the mouse click options. +func (o *MouseClickOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -68,9 +69,10 @@ func NewMouseDblClickOptions() *MouseDblClickOptions { } } -func (o *MouseDblClickOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the mouse double click options. +func (o *MouseDblClickOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -100,9 +102,10 @@ func NewMouseDownUpOptions() *MouseDownUpOptions { } } -func (o *MouseDownUpOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the mouse down/up options. +func (o *MouseDownUpOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -122,9 +125,10 @@ func NewMouseMoveOptions() *MouseMoveOptions { } } -func (o *MouseMoveOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the mouse move options. +func (o *MouseMoveOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { diff --git a/vendor/github.com/grafana/xk6-browser/common/network_manager.go b/vendor/github.com/grafana/xk6-browser/common/network_manager.go index 3930eb0b3d3..dd45126761f 100644 --- a/vendor/github.com/grafana/xk6-browser/common/network_manager.go +++ b/vendor/github.com/grafana/xk6-browser/common/network_manager.go @@ -25,7 +25,7 @@ import ( "github.com/chromedp/cdproto/emulation" "github.com/chromedp/cdproto/fetch" "github.com/chromedp/cdproto/network" - "github.com/dop251/goja" + "github.com/grafana/sobek" ) // Credentials holds HTTP authentication credentials. @@ -39,10 +39,10 @@ func NewCredentials() *Credentials { return &Credentials{} } -// Parse credentials details from a given goja credentials value. -func (c *Credentials) Parse(ctx context.Context, credentials goja.Value) error { +// Parse credentials details from a given sobek credentials value. +func (c *Credentials) Parse(ctx context.Context, credentials sobek.Value) error { rt := k6ext.Runtime(ctx) - if credentials != nil && !goja.IsUndefined(credentials) && !goja.IsNull(credentials) { + if credentials != nil && !sobek.IsUndefined(credentials) && !sobek.IsNull(credentials) { credentials := credentials.ToObject(rt) for _, k := range credentials.Keys() { switch k { @@ -705,34 +705,40 @@ func (m *NetworkManager) updateProtocolRequestInterception() error { } // Authenticate sets HTTP authentication credentials to use. -func (m *NetworkManager) Authenticate(credentials *Credentials) { +func (m *NetworkManager) Authenticate(credentials *Credentials) error { m.credentials = credentials if credentials != nil { m.userReqInterceptionEnabled = true } if err := m.updateProtocolRequestInterception(); err != nil { - k6ext.Panic(m.ctx, "setting authentication credentials: %w", err) + return fmt.Errorf("setting authentication credentials: %w", err) } + + return nil } // ExtraHTTPHeaders returns the currently set extra HTTP request headers. -func (m *NetworkManager) ExtraHTTPHeaders() goja.Value { +func (m *NetworkManager) ExtraHTTPHeaders() sobek.Value { rt := m.vu.Runtime() return rt.ToValue(m.extraHTTPHeaders) } // SetExtraHTTPHeaders sets extra HTTP request headers to be sent with every request. -func (m *NetworkManager) SetExtraHTTPHeaders(headers network.Headers) { - action := network.SetExtraHTTPHeaders(headers) - if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { - k6ext.Panic(m.ctx, "setting extra HTTP headers: %w", err) +func (m *NetworkManager) SetExtraHTTPHeaders(headers network.Headers) error { + err := network. + SetExtraHTTPHeaders(headers). + Do(cdp.WithExecutor(m.ctx, m.session)) + if err != nil { + return fmt.Errorf("setting extra HTTP headers: %w", err) } + + return nil } // SetOfflineMode toggles offline mode on/off. -func (m *NetworkManager) SetOfflineMode(offline bool) { +func (m *NetworkManager) SetOfflineMode(offline bool) error { if m.offline == offline { - return + return nil } m.offline = offline @@ -743,8 +749,10 @@ func (m *NetworkManager) SetOfflineMode(offline bool) { m.networkProfile.Upload, ) if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { - k6ext.Panic(m.ctx, "setting offline mode: %w", err) + return fmt.Errorf("emulating network conditions: %w", err) } + + return nil } // ThrottleNetwork changes the network attributes in chrome to simulate slower diff --git a/vendor/github.com/grafana/xk6-browser/common/page.go b/vendor/github.com/grafana/xk6-browser/common/page.go index 39fa139339c..96585993d6d 100644 --- a/vendor/github.com/grafana/xk6-browser/common/page.go +++ b/vendor/github.com/grafana/xk6-browser/common/page.go @@ -19,7 +19,7 @@ import ( "github.com/chromedp/cdproto/runtime" cdpruntime "github.com/chromedp/cdproto/runtime" "github.com/chromedp/cdproto/target" - "github.com/dop251/goja" + "github.com/grafana/sobek" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -104,9 +104,9 @@ const ( ) // Parse parses the given screen options. -func (s *Screen) Parse(ctx context.Context, screen goja.Value) error { +func (s *Screen) Parse(ctx context.Context, screen sobek.Value) error { rt := k6ext.Runtime(ctx) - if screen != nil && !goja.IsUndefined(screen) && !goja.IsNull(screen) { + if screen != nil && !sobek.IsUndefined(screen) && !sobek.IsNull(screen) { screen := screen.ToObject(rt) for _, k := range screen.Keys() { switch k { @@ -441,7 +441,7 @@ func (p *Page) getFrameElement(f *Frame) (handle *ElementHandle, _ error) { return parent.adoptBackendNodeID(mainWorld, backendNodeId) } -func (p *Page) getOwnerFrame(apiCtx context.Context, h *ElementHandle) cdp.FrameID { +func (p *Page) getOwnerFrame(apiCtx context.Context, h *ElementHandle) (cdp.FrameID, error) { p.logger.Debugf("Page:getOwnerFrame", "sid:%v", p.sessionID()) // document.documentElement has frameId of the owner frame @@ -461,34 +461,37 @@ func (p *Page) getOwnerFrame(apiCtx context.Context, h *ElementHandle) cdp.Frame result, err := h.execCtx.eval(apiCtx, opts, pageFn, h) if err != nil { p.logger.Debugf("Page:getOwnerFrame:return", "sid:%v err:%v", p.sessionID(), err) - return "" + return "", nil } switch result.(type) { case nil: p.logger.Debugf("Page:getOwnerFrame:return", "sid:%v result:nil", p.sessionID()) - return "" + return "", nil } documentElement := result.(*ElementHandle) if documentElement == nil { p.logger.Debugf("Page:getOwnerFrame:return", "sid:%v docel:nil", p.sessionID()) - return "" + return "", nil } if documentElement.remoteObject.ObjectID == "" { p.logger.Debugf("Page:getOwnerFrame:return", "sid:%v robjid:%q", p.sessionID(), "") - return "" + return "", nil } action := dom.DescribeNode().WithObjectID(documentElement.remoteObject.ObjectID) node, err := action.Do(cdp.WithExecutor(p.ctx, p.session)) if err != nil { p.logger.Debugf("Page:getOwnerFrame:DescribeNode:return", "sid:%v err:%v", p.sessionID(), err) - return "" + return "", nil } frameID := node.FrameID - documentElement.Dispose() - return frameID + if err := documentElement.Dispose(); err != nil { + return "", fmt.Errorf("disposing document element while getting owner frame: %w", err) + } + + return frameID, nil } func (p *Page) attachFrameSession(fid cdp.FrameID, fs *FrameSession) { @@ -538,15 +541,19 @@ func (p *Page) setViewportSize(viewportSize *Size) error { return p.setEmulatedSize(NewEmulatedSize(viewport, screen)) } -func (p *Page) updateExtraHTTPHeaders() { +func (p *Page) updateExtraHTTPHeaders() error { p.logger.Debugf("Page:updateExtraHTTPHeaders", "sid:%v", p.sessionID()) p.frameSessionsMu.RLock() defer p.frameSessionsMu.RUnlock() for _, fs := range p.frameSessions { - fs.updateExtraHTTPHeaders(false) + if err := fs.updateExtraHTTPHeaders(false); err != nil { + return fmt.Errorf("updating extra HTTP headers: %w", err) + } } + + return nil } func (p *Page) updateGeolocation() error { @@ -572,26 +579,34 @@ func (p *Page) updateGeolocation() error { return nil } -func (p *Page) updateOffline() { +func (p *Page) updateOffline() error { p.logger.Debugf("Page:updateOffline", "sid:%v", p.sessionID()) p.frameSessionsMu.RLock() defer p.frameSessionsMu.RUnlock() for _, fs := range p.frameSessions { - fs.updateOffline(false) + if err := fs.updateOffline(false); err != nil { + return fmt.Errorf("updating page frame sessions to offline: %w", err) + } } + + return nil } -func (p *Page) updateHttpCredentials() { +func (p *Page) updateHTTPCredentials() error { p.logger.Debugf("Page:updateHttpCredentials", "sid:%v", p.sessionID()) p.frameSessionsMu.RLock() defer p.frameSessionsMu.RUnlock() for _, fs := range p.frameSessions { - fs.updateHTTPCredentials(false) + if err := fs.updateHTTPCredentials(false); err != nil { + return err + } } + + return nil } func (p *Page) viewportSize() Size { @@ -602,32 +617,34 @@ func (p *Page) viewportSize() Size { } // BringToFront activates the browser tab for this page. -func (p *Page) BringToFront() { +func (p *Page) BringToFront() error { p.logger.Debugf("Page:BringToFront", "sid:%v", p.sessionID()) action := cdppage.BringToFront() if err := action.Do(cdp.WithExecutor(p.ctx, p.session)); err != nil { - k6ext.Panic(p.ctx, "bringing page to front: %w", err) + return fmt.Errorf("bringing page to front: %w", err) } + + return nil } // Check checks an element matching the provided selector. -func (p *Page) Check(selector string, opts goja.Value) { +func (p *Page) Check(selector string, opts sobek.Value) error { p.logger.Debugf("Page:Check", "sid:%v selector:%s", p.sessionID(), selector) - p.MainFrame().Check(selector, opts) + return p.MainFrame().Check(selector, opts) } // Uncheck unchecks an element matching the provided selector. -func (p *Page) Uncheck(selector string, opts goja.Value) { +func (p *Page) Uncheck(selector string, opts sobek.Value) error { p.logger.Debugf("Page:Uncheck", "sid:%v selector:%s", p.sessionID(), selector) - p.MainFrame().Uncheck(selector, opts) + return p.MainFrame().Uncheck(selector, opts) } // IsChecked returns true if the first element that matches the selector // is checked. Otherwise, returns false. -func (p *Page) IsChecked(selector string, opts goja.Value) bool { +func (p *Page) IsChecked(selector string, opts sobek.Value) (bool, error) { p.logger.Debugf("Page:IsChecked", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().IsChecked(selector, opts) @@ -641,7 +658,7 @@ func (p *Page) Click(selector string, opts *FrameClickOptions) error { } // Close closes the page. -func (p *Page) Close(_ goja.Value) error { +func (p *Page) Close(_ sobek.Value) error { p.logger.Debugf("Page:Close", "sid:%v", p.sessionID()) _, span := TraceAPICall(p.ctx, p.targetID.String(), "page.close") defer span.End() @@ -687,7 +704,7 @@ func (p *Page) Close(_ goja.Value) error { } // Content returns the HTML content of the page. -func (p *Page) Content() string { +func (p *Page) Content() (string, error) { p.logger.Debugf("Page:Content", "sid:%v", p.sessionID()) return p.MainFrame().Content() @@ -699,10 +716,10 @@ func (p *Page) Context() *BrowserContext { } // Dblclick double clicks an element matching provided selector. -func (p *Page) Dblclick(selector string, opts goja.Value) { +func (p *Page) Dblclick(selector string, opts sobek.Value) error { p.logger.Debugf("Page:Dblclick", "sid:%v selector:%s", p.sessionID(), selector) - p.MainFrame().Dblclick(selector, opts) + return p.MainFrame().Dblclick(selector, opts) } // DispatchEvent dispatches an event on the page to the element that matches the provided selector. @@ -712,12 +729,13 @@ func (p *Page) DispatchEvent(selector string, typ string, eventInit any, opts *F return p.MainFrame().DispatchEvent(selector, typ, eventInit, opts) } -func (p *Page) EmulateMedia(opts goja.Value) { +// EmulateMedia emulates the given media type. +func (p *Page) EmulateMedia(opts sobek.Value) error { p.logger.Debugf("Page:EmulateMedia", "sid:%v", p.sessionID()) parsedOpts := NewPageEmulateMediaOptions(p.mediaType, p.colorScheme, p.reducedMotion) if err := parsedOpts.Parse(p.ctx, opts); err != nil { - k6ext.Panic(p.ctx, "parsing emulateMedia options: %w", err) + return fmt.Errorf("parsing emulateMedia options: %w", err) } p.mediaType = parsedOpts.Media @@ -728,16 +746,18 @@ func (p *Page) EmulateMedia(opts goja.Value) { for _, fs := range p.frameSessions { if err := fs.updateEmulateMedia(false); err != nil { p.frameSessionsMu.RUnlock() - k6ext.Panic(p.ctx, "emulating media: %w", err) + return fmt.Errorf("emulating media: %w", err) } } p.frameSessionsMu.RUnlock() applySlowMo(p.ctx) + + return nil } // EmulateVisionDeficiency activates/deactivates emulation of a vision deficiency. -func (p *Page) EmulateVisionDeficiency(typ string) { +func (p *Page) EmulateVisionDeficiency(typ string) error { p.logger.Debugf("Page:EmulateVisionDeficiency", "sid:%v typ:%s", p.sessionID(), typ) validTypes := map[string]emulation.SetEmulatedVisionDeficiencyType{ @@ -750,19 +770,21 @@ func (p *Page) EmulateVisionDeficiency(typ string) { } t, ok := validTypes[typ] if !ok { - k6ext.Panic(p.ctx, "unsupported vision deficiency: '%s'", typ) + return fmt.Errorf("unsupported vision deficiency: %s", typ) } action := emulation.SetEmulatedVisionDeficiency(t) if err := action.Do(cdp.WithExecutor(p.ctx, p.session)); err != nil { - k6ext.Panic(p.ctx, "setting emulated vision deficiency %q: %w", typ, err) + return fmt.Errorf("setting emulated vision deficiency %q: %w", typ, err) } applySlowMo(p.ctx) + + return nil } // Evaluate runs JS code within the execution context of the main frame of the page. -func (p *Page) Evaluate(pageFunc string, args ...any) any { +func (p *Page) Evaluate(pageFunc string, args ...any) (any, error) { p.logger.Debugf("Page:Evaluate", "sid:%v", p.sessionID()) return p.MainFrame().Evaluate(pageFunc, args...) @@ -779,16 +801,18 @@ func (p *Page) EvaluateHandle(pageFunc string, args ...any) (JSHandleAPI, error) return h, nil } -func (p *Page) Fill(selector string, value string, opts goja.Value) { +// Fill fills an input element with the provided value. +func (p *Page) Fill(selector string, value string, opts sobek.Value) error { p.logger.Debugf("Page:Fill", "sid:%v selector:%s", p.sessionID(), selector) - p.MainFrame().Fill(selector, value, opts) + return p.MainFrame().Fill(selector, value, opts) } -func (p *Page) Focus(selector string, opts goja.Value) { +// Focus focuses an element matching the provided selector. +func (p *Page) Focus(selector string, opts sobek.Value) error { p.logger.Debugf("Page:Focus", "sid:%v selector:%s", p.sessionID(), selector) - p.MainFrame().Focus(selector, opts) + return p.MainFrame().Focus(selector, opts) } // Frames returns a list of frames on the page. @@ -797,7 +821,8 @@ func (p *Page) Frames() []*Frame { } // GetAttribute returns the attribute value of the element matching the provided selector. -func (p *Page) GetAttribute(selector string, name string, opts goja.Value) any { +// The second return value is true if the attribute exists, and false otherwise. +func (p *Page) GetAttribute(selector string, name string, opts sobek.Value) (string, bool, error) { p.logger.Debugf("Page:GetAttribute", "sid:%v selector:%s name:%s", p.sessionID(), selector, name) @@ -839,25 +864,29 @@ func (p *Page) Goto(url string, opts *FrameGotoOptions) (*Response, error) { return resp, nil } -func (p *Page) Hover(selector string, opts goja.Value) { +// Hover hovers over an element matching the provided selector. +func (p *Page) Hover(selector string, opts sobek.Value) error { p.logger.Debugf("Page:Hover", "sid:%v selector:%s", p.sessionID(), selector) - p.MainFrame().Hover(selector, opts) + return p.MainFrame().Hover(selector, opts) } -func (p *Page) InnerHTML(selector string, opts goja.Value) string { +// InnerHTML returns the inner HTML of the element matching the provided selector. +func (p *Page) InnerHTML(selector string, opts sobek.Value) (string, error) { p.logger.Debugf("Page:InnerHTML", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().InnerHTML(selector, opts) } -func (p *Page) InnerText(selector string, opts goja.Value) string { +// InnerText returns the inner text of the element matching the provided selector. +func (p *Page) InnerText(selector string, opts sobek.Value) (string, error) { p.logger.Debugf("Page:InnerText", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().InnerText(selector, opts) } -func (p *Page) InputValue(selector string, opts goja.Value) string { +// InputValue returns the value of the input element matching the provided selector. +func (p *Page) InputValue(selector string, opts sobek.Value) (string, error) { p.logger.Debugf("Page:InputValue", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().InputValue(selector, opts) @@ -870,19 +899,25 @@ func (p *Page) IsClosed() bool { return p.closed } -func (p *Page) IsDisabled(selector string, opts goja.Value) bool { +// IsDisabled returns true if the first element that matches the selector +// is disabled. Otherwise, returns false. +func (p *Page) IsDisabled(selector string, opts sobek.Value) (bool, error) { p.logger.Debugf("Page:IsDisabled", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().IsDisabled(selector, opts) } -func (p *Page) IsEditable(selector string, opts goja.Value) bool { +// IsEditable returns true if the first element that matches the selector +// is editable. Otherwise, returns false. +func (p *Page) IsEditable(selector string, opts sobek.Value) (bool, error) { p.logger.Debugf("Page:IsEditable", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().IsEditable(selector, opts) } -func (p *Page) IsEnabled(selector string, opts goja.Value) bool { +// IsEnabled returns true if the first element that matches the selector +// is enabled. Otherwise, returns false. +func (p *Page) IsEnabled(selector string, opts sobek.Value) (bool, error) { p.logger.Debugf("Page:IsEnabled", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().IsEnabled(selector, opts) @@ -891,7 +926,7 @@ func (p *Page) IsEnabled(selector string, opts goja.Value) bool { // IsHidden will look for an element in the dom with given selector and see if // the element is hidden. It will not wait for a match to occur. If no elements // match `false` will be returned. -func (p *Page) IsHidden(selector string, opts goja.Value) (bool, error) { +func (p *Page) IsHidden(selector string, opts sobek.Value) (bool, error) { p.logger.Debugf("Page:IsHidden", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().IsHidden(selector, opts) @@ -899,14 +934,14 @@ func (p *Page) IsHidden(selector string, opts goja.Value) (bool, error) { // IsVisible will look for an element in the dom with given selector. It will // not wait for a match to occur. If no elements match `false` will be returned. -func (p *Page) IsVisible(selector string, opts goja.Value) (bool, error) { +func (p *Page) IsVisible(selector string, opts sobek.Value) (bool, error) { p.logger.Debugf("Page:IsVisible", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().IsVisible(selector, opts) } // Locator creates and returns a new locator for this page (main frame). -func (p *Page) Locator(selector string, opts goja.Value) *Locator { +func (p *Page) Locator(selector string, opts sobek.Value) *Locator { p.logger.Debugf("Page:Locator", "sid:%s sel: %q opts:%+v", p.sessionID(), selector, opts) return p.MainFrame().Locator(selector, opts) @@ -964,10 +999,11 @@ func (p *Page) Opener() *Page { return p.opener } -func (p *Page) Press(selector string, key string, opts goja.Value) { +// Press presses the given key for the first element found that matches the selector. +func (p *Page) Press(selector string, key string, opts sobek.Value) error { p.logger.Debugf("Page:Press", "sid:%v selector:%s", p.sessionID(), selector) - p.MainFrame().Press(selector, key, opts) + return p.MainFrame().Press(selector, key, opts) } // Query returns the first element matching the specified selector. @@ -985,44 +1021,46 @@ func (p *Page) QueryAll(selector string) ([]*ElementHandle, error) { } // Reload will reload the current page. -func (p *Page) Reload(opts goja.Value) (*Response, error) { //nolint:funlen,cyclop +func (p *Page) Reload(opts sobek.Value) (*Response, error) { //nolint:funlen,cyclop p.logger.Debugf("Page:Reload", "sid:%v", p.sessionID()) _, span := TraceAPICall(p.ctx, p.targetID.String(), "page.reload") defer span.End() - parsedOpts := NewPageReloadOptions( + reloadOpts := NewPageReloadOptions( LifecycleEventLoad, p.timeoutSettings.navigationTimeout(), ) - if err := parsedOpts.Parse(p.ctx, opts); err != nil { + if err := reloadOpts.Parse(p.ctx, opts); err != nil { err := fmt.Errorf("parsing reload options: %w", err) spanRecordError(span, err) return nil, err } - timeoutCtx, timeoutCancelFn := context.WithTimeout(p.ctx, parsedOpts.Timeout) + timeoutCtx, timeoutCancelFn := context.WithTimeout(p.ctx, reloadOpts.Timeout) defer timeoutCancelFn() - ch, evCancelFn := createWaitForEventHandler( - timeoutCtx, p.frameManager.MainFrame(), []string{EventFrameNavigation}, - func(data any) bool { + waitForFrameNavigation, cancelWaitingForFrameNavigation := createWaitForEventHandler( + timeoutCtx, p.frameManager.MainFrame(), + []string{EventFrameNavigation}, + func(_ any) bool { return true // Both successful and failed navigations are considered }, ) - defer evCancelFn() // Remove event handler + defer cancelWaitingForFrameNavigation() // Remove event handler - lifecycleEvtCh, lifecycleEvtCancel := createWaitForEventPredicateHandler( - timeoutCtx, p.frameManager.MainFrame(), []string{EventFrameAddLifecycle}, + waitForLifecycleEvent, cancelWaitingForLifecycleEvent := createWaitForEventPredicateHandler( + timeoutCtx, p.frameManager.MainFrame(), + []string{EventFrameAddLifecycle}, func(data any) bool { if le, ok := data.(FrameLifecycleEvent); ok { - return le.Event == parsedOpts.WaitUntil + return le.Event == reloadOpts.WaitUntil } return false }) - defer lifecycleEvtCancel() + defer cancelWaitingForLifecycleEvent() - action := cdppage.Reload() - if err := action.Do(cdp.WithExecutor(p.ctx, p.session)); err != nil { + reloadAction := cdppage.Reload() + if err := reloadAction.Do(cdp.WithExecutor(p.ctx, p.session)); err != nil { err := fmt.Errorf("reloading page: %w", err) spanRecordError(span, err) return nil, err @@ -1032,36 +1070,43 @@ func (p *Page) Reload(opts goja.Value) (*Response, error) { //nolint:funlen,cycl if errors.Is(err, context.DeadlineExceeded) { err = &k6ext.UserFriendlyError{ Err: err, - Timeout: parsedOpts.Timeout, + Timeout: reloadOpts.Timeout, } return fmt.Errorf("reloading page: %w", err) } p.logger.Debugf("Page:Reload", "timeoutCtx done: %v", err) - return err // TODO maybe wrap this as well? + return fmt.Errorf("reloading page: %w", err) } - var event *NavigationEvent + var ( + navigationEvent *NavigationEvent + err error + ) select { case <-p.ctx.Done(): case <-timeoutCtx.Done(): - err := wrapTimeoutError(timeoutCtx.Err()) + err = wrapTimeoutError(timeoutCtx.Err()) + case event := <-waitForFrameNavigation: + var ok bool + if navigationEvent, ok = event.(*NavigationEvent); !ok { + err = fmt.Errorf("unexpected event data type: %T, expected *NavigationEvent", event) + } + } + if err != nil { spanRecordError(span, err) return nil, err - case data := <-ch: - event = data.(*NavigationEvent) } var resp *Response - req := event.newDocument.request - if req != nil { + if req := navigationEvent.newDocument.request; req != nil { req.responseMu.RLock() resp = req.response req.responseMu.RUnlock() } select { - case <-lifecycleEvtCh: + case <-waitForLifecycleEvent: case <-timeoutCtx.Done(): err := wrapTimeoutError(timeoutCtx.Err()) spanRecordError(span, err) @@ -1091,16 +1136,19 @@ func (p *Page) Screenshot(opts *PageScreenshotOptions, sp ScreenshotPersister) ( return buf, err } -func (p *Page) SelectOption(selector string, values goja.Value, opts goja.Value) []string { +// SelectOption selects the given options and returns the array of +// option values of the first element found that matches the selector. +func (p *Page) SelectOption(selector string, values sobek.Value, opts sobek.Value) ([]string, error) { p.logger.Debugf("Page:SelectOption", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().SelectOption(selector, values, opts) } -func (p *Page) SetContent(html string, opts goja.Value) { +// SetContent replaces the entire HTML document content. +func (p *Page) SetContent(html string, opts sobek.Value) error { p.logger.Debugf("Page:SetContent", "sid:%v", p.sessionID()) - p.MainFrame().SetContent(html, opts) + return p.MainFrame().SetContent(html, opts) } // SetDefaultNavigationTimeout sets the default navigation timeout in milliseconds. @@ -1118,32 +1166,33 @@ func (p *Page) SetDefaultTimeout(timeout int64) { } // SetExtraHTTPHeaders sets default HTTP headers for page and whole frame hierarchy. -func (p *Page) SetExtraHTTPHeaders(headers map[string]string) { +func (p *Page) SetExtraHTTPHeaders(headers map[string]string) error { p.logger.Debugf("Page:SetExtraHTTPHeaders", "sid:%v", p.sessionID()) p.extraHTTPHeaders = headers - p.updateExtraHTTPHeaders() + return p.updateExtraHTTPHeaders() } // SetInputFiles sets input files for the selected element. -func (p *Page) SetInputFiles(selector string, files goja.Value, opts goja.Value) error { +func (p *Page) SetInputFiles(selector string, files sobek.Value, opts sobek.Value) error { p.logger.Debugf("Page:SetInputFiles", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().SetInputFiles(selector, files, opts) } // SetViewportSize will update the viewport width and height. -func (p *Page) SetViewportSize(viewportSize goja.Value) { +func (p *Page) SetViewportSize(viewportSize sobek.Value) error { p.logger.Debugf("Page:SetViewportSize", "sid:%v", p.sessionID()) - s := &Size{} + var s Size if err := s.Parse(p.ctx, viewportSize); err != nil { - k6ext.Panic(p.ctx, "parsing viewport size: %w", err) + return fmt.Errorf("parsing viewport size: %w", err) } - if err := p.setViewportSize(s); err != nil { - k6ext.Panic(p.ctx, "setting viewport size: %w", err) + if err := p.setViewportSize(&s); err != nil { + return fmt.Errorf("setting viewport size: %w", err) } - applySlowMo(p.ctx) + + return nil } // Tap will tap the element matching the provided selector. @@ -1153,7 +1202,10 @@ func (p *Page) Tap(selector string, opts *FrameTapOptions) error { return p.MainFrame().Tap(selector, opts) } -func (p *Page) TextContent(selector string, opts goja.Value) string { +// TextContent returns the textContent attribute of the first element found +// that matches the selector. The second return value is true if the returned +// text content is not null or empty, and false otherwise. +func (p *Page) TextContent(selector string, opts sobek.Value) (string, bool, error) { p.logger.Debugf("Page:TextContent", "sid:%v selector:%s", p.sessionID(), selector) return p.MainFrame().TextContent(selector, opts) @@ -1165,13 +1217,21 @@ func (p *Page) Timeout() time.Duration { return p.defaultTimeout() } -func (p *Page) Title() string { +// Title returns the page title. +func (p *Page) Title() (string, error) { p.logger.Debugf("Page:Title", "sid:%v", p.sessionID()) - // TODO: return error + js := `() => document.title` + v, err := p.Evaluate(js) + if err != nil { + return "", fmt.Errorf("getting page title: %w", err) + } + s, ok := v.(string) + if !ok { + return "", fmt.Errorf("getting page title: expected string, got %T", v) + } - v := `() => document.title` - return p.Evaluate(v).(string) //nolint:forcetypeassert + return s, nil } // ThrottleCPU will slow the CPU down from chrome's perspective to simulate @@ -1208,20 +1268,28 @@ func (p *Page) ThrottleNetwork(networkProfile NetworkProfile) error { return nil } -func (p *Page) Type(selector string, text string, opts goja.Value) { +// Type text on the first element found matches the selector. +func (p *Page) Type(selector string, text string, opts sobek.Value) error { p.logger.Debugf("Page:Type", "sid:%v selector:%s text:%s", p.sessionID(), selector, text) - p.MainFrame().Type(selector, text, opts) + return p.MainFrame().Type(selector, text, opts) } // URL returns the location of the page. -func (p *Page) URL() string { +func (p *Page) URL() (string, error) { p.logger.Debugf("Page:URL", "sid:%v", p.sessionID()) - // TODO: return error + js := `() => document.location.toString()` + v, err := p.Evaluate(js) + if err != nil { + return "", fmt.Errorf("getting page URL: %w", err) + } + s, ok := v.(string) + if !ok { + return "", fmt.Errorf("getting page URL: expected string, got %T", v) + } - v := `() => document.location.toString()` - return p.Evaluate(v).(string) //nolint:forcetypeassert + return s, nil } // ViewportSize will return information on the viewport width and height. @@ -1242,10 +1310,10 @@ func (p *Page) WaitForFunction(js string, opts *FrameWaitForFunctionOptions, jsA } // WaitForLoadState waits for the specified page life cycle event. -func (p *Page) WaitForLoadState(state string, opts goja.Value) { +func (p *Page) WaitForLoadState(state string, opts sobek.Value) error { p.logger.Debugf("Page:WaitForLoadState", "sid:%v state:%q", p.sessionID(), state) - p.frameManager.MainFrame().WaitForLoadState(state, opts) + return p.frameManager.MainFrame().WaitForLoadState(state, opts) } // WaitForNavigation waits for the given navigation lifecycle event to happen. @@ -1264,7 +1332,7 @@ func (p *Page) WaitForNavigation(opts *FrameWaitForNavigationOptions) (*Response } // WaitForSelector waits for the given selector to match the waiting criteria. -func (p *Page) WaitForSelector(selector string, opts goja.Value) (*ElementHandle, error) { +func (p *Page) WaitForSelector(selector string, opts sobek.Value) (*ElementHandle, error) { p.logger.Debugf("Page:WaitForSelector", "sid:%v stid:%v ptid:%v selector:%s", p.sessionID(), p.session.TargetID(), p.targetID, selector) diff --git a/vendor/github.com/grafana/xk6-browser/common/page_options.go b/vendor/github.com/grafana/xk6-browser/common/page_options.go index cca1bdfe29d..d1a2f643ff4 100644 --- a/vendor/github.com/grafana/xk6-browser/common/page_options.go +++ b/vendor/github.com/grafana/xk6-browser/common/page_options.go @@ -7,7 +7,7 @@ import ( "time" "github.com/chromedp/cdproto/page" - "github.com/dop251/goja" + "github.com/grafana/sobek" "github.com/grafana/xk6-browser/k6ext" ) @@ -40,9 +40,10 @@ func NewPageEmulateMediaOptions(defaultMedia MediaType, defaultColorScheme Color } } -func (o *PageEmulateMediaOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the page emulate media options. +func (o *PageEmulateMediaOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -65,9 +66,10 @@ func NewPageReloadOptions(defaultWaitUntil LifecycleEvent, defaultTimeout time.D } } -func (o *PageReloadOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the page reload options. +func (o *PageReloadOptions) Parse(ctx context.Context, opts sobek.Value) error { rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { + if opts != nil && !sobek.IsUndefined(opts) && !sobek.IsNull(opts) { opts := opts.ToObject(rt) for _, k := range opts.Keys() { switch k { @@ -97,45 +99,48 @@ func NewPageScreenshotOptions() *PageScreenshotOptions { } } -func (o *PageScreenshotOptions) Parse(ctx context.Context, opts goja.Value) error { +// Parse parses the page screenshot options. +func (o *PageScreenshotOptions) Parse(ctx context.Context, opts sobek.Value) error { //nolint:cyclop + if !sobekValueExists(opts) { + return nil + } + rt := k6ext.Runtime(ctx) - if opts != nil && !goja.IsUndefined(opts) && !goja.IsNull(opts) { - formatSpecified := false - opts := opts.ToObject(rt) - for _, k := range opts.Keys() { - switch k { - case "clip": - var c map[string]float64 - if rt.ExportTo(opts.Get(k), &c) != nil { - o.Clip = &page.Viewport{ - X: c["x"], - Y: c["y"], - Width: c["width"], - Height: c["height"], - Scale: 1, - } - } - case "fullPage": - o.FullPage = opts.Get(k).ToBoolean() - case "omitBackground": - o.OmitBackground = opts.Get(k).ToBoolean() - case "path": - o.Path = opts.Get(k).String() - case "quality": - o.Quality = opts.Get(k).ToInteger() - case "type": - if f, ok := imageFormatToID[opts.Get(k).String()]; ok { - o.Format = f - formatSpecified = true + formatSpecified := false + obj := opts.ToObject(rt) + for _, k := range obj.Keys() { + switch k { + case "clip": + var c map[string]float64 + if rt.ExportTo(obj.Get(k), &c) != nil { + o.Clip = &page.Viewport{ + X: c["x"], + Y: c["y"], + Width: c["width"], + Height: c["height"], + Scale: 1, } } + case "fullPage": + o.FullPage = obj.Get(k).ToBoolean() + case "omitBackground": + o.OmitBackground = obj.Get(k).ToBoolean() + case "path": + o.Path = obj.Get(k).String() + case "quality": + o.Quality = obj.Get(k).ToInteger() + case "type": + if f, ok := imageFormatToID[obj.Get(k).String()]; ok { + o.Format = f + formatSpecified = true + } } + } - // Infer file format by path if format not explicitly specified (default is PNG) - if o.Path != "" && !formatSpecified { - if strings.HasSuffix(o.Path, ".jpg") || strings.HasSuffix(o.Path, ".jpeg") { - o.Format = ImageFormatJPEG - } + // Infer file format by path if format not explicitly specified (default is PNG) + if o.Path != "" && !formatSpecified { + if strings.HasSuffix(o.Path, ".jpg") || strings.HasSuffix(o.Path, ".jpeg") { + o.Format = ImageFormatJPEG } } diff --git a/vendor/github.com/grafana/xk6-browser/common/remote_object.go b/vendor/github.com/grafana/xk6-browser/common/remote_object.go index 76a649e6687..19bb6f16124 100644 --- a/vendor/github.com/grafana/xk6-browser/common/remote_object.go +++ b/vendor/github.com/grafana/xk6-browser/common/remote_object.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/xk6-browser/log" + "github.com/chromedp/cdproto/runtime" cdpruntime "github.com/chromedp/cdproto/runtime" ) @@ -105,8 +106,8 @@ func parseRemoteObjectValue( if val == "Object" { return val, nil } - if st == "null" { - return "null", nil + if st == runtime.SubtypeNull { + return nil, nil //nolint:nilnil } case cdpruntime.TypeUndefined: return "undefined", nil diff --git a/vendor/github.com/grafana/xk6-browser/common/screenshotter.go b/vendor/github.com/grafana/xk6-browser/common/screenshotter.go index d25b79a3ba2..4b4002146d2 100644 --- a/vendor/github.com/grafana/xk6-browser/common/screenshotter.go +++ b/vendor/github.com/grafana/xk6-browser/common/screenshotter.go @@ -282,18 +282,29 @@ func (s *screenshotter) screenshotElement(h *ElementHandle, opts *ElementHandleS } } - documentRect := bbox - scrollOffset := h.Evaluate(`() => { return {x: window.scrollX, y: window.scrollY};}`) + scrollOffset, err := h.Evaluate(`() => { return {x: window.scrollX, y: window.scrollY};}`) + if err != nil { + return nil, fmt.Errorf("evaluating scroll offset: %w", err) + } var returnVal Position if err := convert(scrollOffset, &returnVal); err != nil { return nil, fmt.Errorf("unpacking scroll offset: %w", err) } + documentRect := bbox documentRect.X += returnVal.X documentRect.Y += returnVal.Y - buf, err := s.screenshot(h.frame.page.session, documentRect.enclosingIntRect(), nil, format, opts.OmitBackground, opts.Quality, opts.Path) + buf, err := s.screenshot( + h.frame.page.session, + documentRect.enclosingIntRect(), + nil, // viewportRect + format, + opts.OmitBackground, + opts.Quality, + opts.Path, + ) if err != nil { return nil, err } @@ -302,6 +313,7 @@ func (s *screenshotter) screenshotElement(h *ElementHandle, opts *ElementHandleS return nil, fmt.Errorf("restoring viewport: %w", err) } } + return buf, nil } diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/context.go b/vendor/github.com/grafana/xk6-browser/k6ext/context.go index 8f763bcdc29..f4570c59a89 100644 --- a/vendor/github.com/grafana/xk6-browser/k6ext/context.go +++ b/vendor/github.com/grafana/xk6-browser/k6ext/context.go @@ -6,7 +6,7 @@ import ( k6modules "go.k6.io/k6/js/modules" k6lib "go.k6.io/k6/lib" - "github.com/dop251/goja" + "github.com/grafana/sobek" ) type ctxKey int @@ -23,7 +23,7 @@ func WithVU(ctx context.Context, vu k6modules.VU) context.Context { } // GetVU returns the attached k6 VU instance from ctx, which can be used to -// retrieve the goja runtime and other k6 objects relevant to the currently +// retrieve the sobek runtime and other k6 objects relevant to the currently // executing VU. // See https://github.com/grafana/k6/blob/v0.37.0/js/initcontext.go#L168-L186 func GetVU(ctx context.Context) k6modules.VU { @@ -49,7 +49,7 @@ func GetCustomMetrics(ctx context.Context) *CustomMetrics { } // Runtime is a convenience function for getting a k6 VU runtime. -func Runtime(ctx context.Context) *goja.Runtime { +func Runtime(ctx context.Context) *sobek.Runtime { return GetVU(ctx).Runtime() } diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/panic.go b/vendor/github.com/grafana/xk6-browser/k6ext/panic.go index 53611c88fb6..78056802626 100644 --- a/vendor/github.com/grafana/xk6-browser/k6ext/panic.go +++ b/vendor/github.com/grafana/xk6-browser/k6ext/panic.go @@ -8,18 +8,18 @@ import ( "strings" "time" - "github.com/dop251/goja" + "github.com/grafana/sobek" "go.k6.io/k6/errext" k6common "go.k6.io/k6/js/common" ) // Abort will shutdown the whole test run. This should -// only be used from the goja mapping layer. It is only +// only be used from the sobek mapping layer. It is only // to be used when an error will occur in all iterations, // so it's permanent. func Abort(ctx context.Context, format string, a ...any) { - failFunc := func(rt *goja.Runtime, a ...any) { + failFunc := func(rt *sobek.Runtime, a ...any) { reason := fmt.Errorf(format, a...).Error() rt.Interrupt(&errext.InterruptError{Reason: reason}) } @@ -31,13 +31,13 @@ func Abort(ctx context.Context, format string, a ...any) { // browser process from the context and kill it if it still exists. // TODO: test. func Panic(ctx context.Context, format string, a ...any) { - failFunc := func(rt *goja.Runtime, a ...any) { + failFunc := func(rt *sobek.Runtime, a ...any) { k6common.Throw(rt, fmt.Errorf(format, a...)) } sharedPanic(ctx, failFunc, a...) } -func sharedPanic(ctx context.Context, failFunc func(rt *goja.Runtime, a ...any), a ...any) { +func sharedPanic(ctx context.Context, failFunc func(rt *sobek.Runtime, a ...any), a ...any) { rt := Runtime(ctx) if rt == nil { // this should never happen unless a programmer error diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/promise.go b/vendor/github.com/grafana/xk6-browser/k6ext/promise.go index 6b4465f4061..5aca02218b8 100644 --- a/vendor/github.com/grafana/xk6-browser/k6ext/promise.go +++ b/vendor/github.com/grafana/xk6-browser/k6ext/promise.go @@ -3,7 +3,7 @@ package k6ext import ( "context" - "github.com/dop251/goja" + "github.com/grafana/sobek" ) // eventLoopDirective determines whether the event @@ -18,20 +18,20 @@ const ( // PromisifiedFunc is a type of the function to run as a promise. type PromisifiedFunc func() (result any, reason error) -// Promise runs fn in a goroutine and returns a new goja.Promise. +// Promise runs fn in a goroutine and returns a new sobek.Promise. // - If fn returns a nil error, resolves the promise with the // first result value fn returns. // - Otherwise, rejects the promise with the error fn returns. -func Promise(ctx context.Context, fn PromisifiedFunc) *goja.Promise { +func Promise(ctx context.Context, fn PromisifiedFunc) *sobek.Promise { return promise(ctx, fn, continueEventLoop) } // AbortingPromise is like Promise, but it aborts the event loop if an error occurs. -func AbortingPromise(ctx context.Context, fn PromisifiedFunc) *goja.Promise { +func AbortingPromise(ctx context.Context, fn PromisifiedFunc) *sobek.Promise { return promise(ctx, fn, abortEventLoop) } -func promise(ctx context.Context, fn PromisifiedFunc, d eventLoopDirective) *goja.Promise { +func promise(ctx context.Context, fn PromisifiedFunc, d eventLoopDirective) *sobek.Promise { var ( vu = GetVU(ctx) cb = vu.RegisterCallback() diff --git a/vendor/modules.txt b/vendor/modules.txt index 98344593324..89a220e5aa3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -176,7 +176,7 @@ github.com/gorilla/websocket # github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78 ## explicit; go 1.20 github.com/grafana/sobek -# github.com/grafana/xk6-browser v1.5.1 +# github.com/grafana/xk6-browser v1.5.2-0.20240607140836-ffcc1f5169ad ## explicit; go 1.20 github.com/grafana/xk6-browser/browser github.com/grafana/xk6-browser/chromium