From 5c6f6796f2c0398040ad8c5a5c5295cfbb0f7181 Mon Sep 17 00:00:00 2001 From: na-- Date: Fri, 31 Mar 2023 00:10:41 -0800 Subject: [PATCH] Update xk6-browser to the latest main version (#2994) We need this, so we can refactor some logging code --- go.mod | 2 +- go.sum | 5 +- .../grafana/xk6-browser/api/browser.go | 4 +- .../xk6-browser/api/browser_context.go | 8 +- .../grafana/xk6-browser/api/browser_type.go | 4 +- .../grafana/xk6-browser/api/element_handle.go | 10 +- .../grafana/xk6-browser/api/frame.go | 10 +- .../grafana/xk6-browser/api/js_handle.go | 4 +- .../grafana/xk6-browser/api/locator.go | 2 +- .../grafana/xk6-browser/api/page.go | 10 +- .../grafana/xk6-browser/api/worker.go | 2 +- .../grafana/xk6-browser/browser/mapping.go | 351 +++++++++++------- .../grafana/xk6-browser/browser/module.go | 21 +- .../grafana/xk6-browser/browser/modulevu.go | 29 ++ .../xk6-browser/browser/pidregistry.go | 28 ++ .../xk6-browser/chromium/browser_type.go | 252 ++++++++----- .../grafana/xk6-browser/common/browser.go | 59 ++- .../xk6-browser/common/browser_context.go | 115 +++++- .../xk6-browser/common/browser_options.go | 83 +++-- .../xk6-browser/common/browser_process.go | 76 ++-- .../common/browser_process_meta.go | 70 ++++ .../xk6-browser/common/element_handle.go | 96 +++-- .../xk6-browser/common/execution_context.go | 4 + .../grafana/xk6-browser/common/frame.go | 57 +-- .../xk6-browser/common/frame_session.go | 74 +++- .../xk6-browser/common/js/web_vital.go | 19 + .../xk6-browser/common/js/web_vital_iife.js | 1 + .../xk6-browser/common/js/web_vital_init.js | 25 ++ .../grafana/xk6-browser/common/js_handle.go | 16 +- .../grafana/xk6-browser/common/keyboard.go | 4 +- .../grafana/xk6-browser/common/locator.go | 19 +- .../grafana/xk6-browser/common/page.go | 70 +++- .../grafana/xk6-browser/common/session.go | 8 - .../grafana/xk6-browser/common/worker.go | 5 +- .../grafana/xk6-browser/k6error/internal.go | 15 + .../grafana/xk6-browser/k6ext/context.go | 11 - .../grafana/xk6-browser/k6ext/metrics.go | 57 ++- .../grafana/xk6-browser/k6ext/panic.go | 57 ++- .../grafana/xk6-browser/keyboardlayout/us.go | 210 +++++------ .../grafana/xk6-browser/log/logger.go | 22 +- vendor/modules.txt | 3 +- 41 files changed, 1312 insertions(+), 606 deletions(-) create mode 100644 vendor/github.com/grafana/xk6-browser/browser/modulevu.go create mode 100644 vendor/github.com/grafana/xk6-browser/browser/pidregistry.go create mode 100644 vendor/github.com/grafana/xk6-browser/common/browser_process_meta.go create mode 100644 vendor/github.com/grafana/xk6-browser/common/js/web_vital.go create mode 100644 vendor/github.com/grafana/xk6-browser/common/js/web_vital_iife.js create mode 100644 vendor/github.com/grafana/xk6-browser/common/js/web_vital_init.js create mode 100644 vendor/github.com/grafana/xk6-browser/k6error/internal.go diff --git a/go.mod b/go.mod index c353095955c..35599c330c8 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/go-sourcemap/sourcemap v2.1.4-0.20211119122758-180fcef48034+incompatible github.com/golang/protobuf v1.5.2 github.com/gorilla/websocket v1.5.0 - github.com/grafana/xk6-browser v0.8.1-0.20230207135343-cfd6a83dfc42 + github.com/grafana/xk6-browser v0.8.2-0.20230329135657-a01218eaee2f github.com/grafana/xk6-output-prometheus-remote v0.1.0 github.com/grafana/xk6-redis v0.1.1 github.com/grafana/xk6-timers v0.1.2 diff --git a/go.sum b/go.sum index c297136f797..e4152d8ebc8 100644 --- a/go.sum +++ b/go.sum @@ -168,8 +168,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/xk6-browser v0.8.1-0.20230207135343-cfd6a83dfc42 h1:y62cvumZOkhwh5p4bOcURP3J/DceFUsbOjFSds7+NMs= -github.com/grafana/xk6-browser v0.8.1-0.20230207135343-cfd6a83dfc42/go.mod h1:elvssKBUHI8rLShlWb+lOUhkaGcn0RcipKE6MRcezVw= +github.com/grafana/xk6-browser v0.8.2-0.20230329135657-a01218eaee2f h1:/9pTQhJoYlB3vrubQXAODDZEl4pcKdm0sNN+8e0+xtI= +github.com/grafana/xk6-browser v0.8.2-0.20230329135657-a01218eaee2f/go.mod h1:W5hLYHj3JT1wivOncQan8OncXBa39rjdRM4qwyXi6nI= github.com/grafana/xk6-output-prometheus-remote v0.1.0 h1:yJc09O6TeBYLFfNG/dqBDtvHmM9P1B2ZFTyr0HgvsHY= github.com/grafana/xk6-output-prometheus-remote v0.1.0/go.mod h1:R4o0VbIfbQNNPSGkeeiCBLzwNfG+DEdfKYNsV1oww1Y= github.com/grafana/xk6-redis v0.1.1 h1:rvWnLanRB2qzDwuY6NMBe6PXei3wJ3kjYvfCwRJ+q+8= @@ -242,7 +242,6 @@ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/vendor/github.com/grafana/xk6-browser/api/browser.go b/vendor/github.com/grafana/xk6-browser/api/browser.go index c601fb6cce0..7b60fca75cd 100644 --- a/vendor/github.com/grafana/xk6-browser/api/browser.go +++ b/vendor/github.com/grafana/xk6-browser/api/browser.go @@ -7,8 +7,8 @@ type Browser interface { Close() Contexts() []BrowserContext IsConnected() bool - NewContext(opts goja.Value) BrowserContext - NewPage(opts goja.Value) Page + NewContext(opts goja.Value) (BrowserContext, error) + NewPage(opts goja.Value) (Page, error) On(string) (bool, error) UserAgent() string Version() string diff --git a/vendor/github.com/grafana/xk6-browser/api/browser_context.go b/vendor/github.com/grafana/xk6-browser/api/browser_context.go index a3aa39a0734..bef11b173db 100644 --- a/vendor/github.com/grafana/xk6-browser/api/browser_context.go +++ b/vendor/github.com/grafana/xk6-browser/api/browser_context.go @@ -7,22 +7,22 @@ import ( // BrowserContext is the public interface of a CDP browser context. type BrowserContext interface { AddCookies(cookies goja.Value) - AddInitScript(script goja.Value, arg goja.Value) + AddInitScript(script goja.Value, arg goja.Value) error Browser() Browser ClearCookies() ClearPermissions() Close() - Cookies() []any // TODO: make it []Cookie later on + Cookies() ([]any, error) // TODO: make it []Cookie later on ExposeBinding(name string, callback goja.Callable, opts goja.Value) ExposeFunction(name string, callback goja.Callable) GrantPermissions(permissions []string, opts goja.Value) NewCDPSession() CDPSession - NewPage() Page + NewPage() (Page, error) Pages() []Page Route(url goja.Value, handler goja.Callable) SetDefaultNavigationTimeout(timeout int64) SetDefaultTimeout(timeout int64) - SetExtraHTTPHeaders(headers map[string]string) + SetExtraHTTPHeaders(headers map[string]string) error SetGeolocation(geolocation goja.Value) // SetHTTPCredentials sets username/password credentials to use for HTTP authentication. // diff --git a/vendor/github.com/grafana/xk6-browser/api/browser_type.go b/vendor/github.com/grafana/xk6-browser/api/browser_type.go index a506922bb7f..07f047c3da8 100644 --- a/vendor/github.com/grafana/xk6-browser/api/browser_type.go +++ b/vendor/github.com/grafana/xk6-browser/api/browser_type.go @@ -6,9 +6,9 @@ import ( // BrowserType is the public interface of a CDP browser client. type BrowserType interface { - Connect(opts goja.Value) + Connect(wsEndpoint string, opts goja.Value) Browser ExecutablePath() string - Launch(opts goja.Value) Browser + Launch(opts goja.Value) (_ Browser, browserProcessID int) LaunchPersistentContext(userDataDir string, opts goja.Value) Browser Name() string } diff --git a/vendor/github.com/grafana/xk6-browser/api/element_handle.go b/vendor/github.com/grafana/xk6-browser/api/element_handle.go index b9e563426ca..9b6d48a7f6e 100644 --- a/vendor/github.com/grafana/xk6-browser/api/element_handle.go +++ b/vendor/github.com/grafana/xk6-browser/api/element_handle.go @@ -9,7 +9,7 @@ type ElementHandle interface { BoundingBox() *Rect Check(opts goja.Value) Click(opts goja.Value) error - ContentFrame() Frame + ContentFrame() (Frame, error) Dblclick(opts goja.Value) DispatchEvent(typ string, props goja.Value) Fill(value string, opts goja.Value) @@ -25,10 +25,10 @@ type ElementHandle interface { IsEnabled() bool IsHidden() bool IsVisible() bool - OwnerFrame() Frame + OwnerFrame() (Frame, error) Press(key string, opts goja.Value) - Query(selector string) ElementHandle - QueryAll(selector string) []ElementHandle + Query(selector string) (ElementHandle, error) + QueryAll(selector string) ([]ElementHandle, error) Screenshot(opts goja.Value) goja.ArrayBuffer ScrollIntoViewIfNeeded(opts goja.Value) SelectOption(values goja.Value, opts goja.Value) []string @@ -39,5 +39,5 @@ type ElementHandle interface { Type(text string, opts goja.Value) Uncheck(opts goja.Value) WaitForElementState(state string, opts goja.Value) - WaitForSelector(selector string, opts goja.Value) ElementHandle + WaitForSelector(selector string, opts goja.Value) (ElementHandle, error) } diff --git a/vendor/github.com/grafana/xk6-browser/api/frame.go b/vendor/github.com/grafana/xk6-browser/api/frame.go index 5f474a7fce2..a8e44acb93e 100644 --- a/vendor/github.com/grafana/xk6-browser/api/frame.go +++ b/vendor/github.com/grafana/xk6-browser/api/frame.go @@ -13,10 +13,10 @@ type Frame interface { Dblclick(selector string, opts goja.Value) DispatchEvent(selector string, typ string, eventInit goja.Value, opts goja.Value) Evaluate(pageFunc goja.Value, args ...goja.Value) any - EvaluateHandle(pageFunc goja.Value, args ...goja.Value) JSHandle + EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (JSHandle, error) Fill(selector string, value string, opts goja.Value) Focus(selector string, opts goja.Value) - FrameElement() ElementHandle + FrameElement() (ElementHandle, error) GetAttribute(selector string, name string, opts goja.Value) goja.Value Goto(url string, opts goja.Value) (Response, error) Hover(selector string, opts goja.Value) @@ -35,8 +35,8 @@ type Frame interface { // Locator creates and returns a new locator for this frame. Locator(selector string, opts goja.Value) Locator Name() string - Query(selector string) ElementHandle - QueryAll(selector string) []ElementHandle + Query(selector string) (ElementHandle, error) + QueryAll(selector string) ([]ElementHandle, error) Page() Page ParentFrame() Frame Press(selector string, key string, opts goja.Value) @@ -52,6 +52,6 @@ type Frame interface { WaitForFunction(pageFunc, opts goja.Value, args ...goja.Value) (any, error) WaitForLoadState(state string, opts goja.Value) WaitForNavigation(opts goja.Value) (Response, error) - WaitForSelector(selector string, opts goja.Value) ElementHandle + WaitForSelector(selector string, opts goja.Value) (ElementHandle, error) WaitForTimeout(timeout int64) } diff --git a/vendor/github.com/grafana/xk6-browser/api/js_handle.go b/vendor/github.com/grafana/xk6-browser/api/js_handle.go index 8c9a5464b42..61ac87192b5 100644 --- a/vendor/github.com/grafana/xk6-browser/api/js_handle.go +++ b/vendor/github.com/grafana/xk6-browser/api/js_handle.go @@ -10,8 +10,8 @@ type JSHandle interface { AsElement() ElementHandle Dispose() Evaluate(pageFunc goja.Value, args ...goja.Value) any - EvaluateHandle(pageFunc goja.Value, args ...goja.Value) JSHandle - GetProperties() map[string]JSHandle + EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (JSHandle, error) + GetProperties() (map[string]JSHandle, error) GetProperty(propertyName string) JSHandle JSONValue() goja.Value ObjectID() cdpruntime.RemoteObjectID diff --git a/vendor/github.com/grafana/xk6-browser/api/locator.go b/vendor/github.com/grafana/xk6-browser/api/locator.go index 93a975cd912..468d8c95205 100644 --- a/vendor/github.com/grafana/xk6-browser/api/locator.go +++ b/vendor/github.com/grafana/xk6-browser/api/locator.go @@ -11,7 +11,7 @@ import "github.com/dop251/goja" // Locator represents a way to find element(s) on a page at any moment. type Locator interface { // Click on an element using locator's selector with strict mode on. - Click(opts goja.Value) + Click(opts goja.Value) error // Dblclick double clicks on an element using locator's selector with strict mode on. Dblclick(opts goja.Value) // Check element using locator's selector with strict mode on. diff --git a/vendor/github.com/grafana/xk6-browser/api/page.go b/vendor/github.com/grafana/xk6-browser/api/page.go index af056dddaff..37580839b87 100644 --- a/vendor/github.com/grafana/xk6-browser/api/page.go +++ b/vendor/github.com/grafana/xk6-browser/api/page.go @@ -10,7 +10,7 @@ type Page interface { BringToFront() Check(selector string, opts goja.Value) Click(selector string, opts goja.Value) error - Close(opts goja.Value) + Close(opts goja.Value) error Content() string Context() BrowserContext Dblclick(selector string, opts goja.Value) @@ -19,7 +19,7 @@ type Page interface { EmulateMedia(opts goja.Value) EmulateVisionDeficiency(typ string) Evaluate(pageFunc goja.Value, arg ...goja.Value) any - EvaluateHandle(pageFunc goja.Value, arg ...goja.Value) JSHandle + EvaluateHandle(pageFunc goja.Value, arg ...goja.Value) (JSHandle, error) ExposeBinding(name string, callback goja.Callable, opts goja.Value) ExposeFunction(name string, callback goja.Callable) Fill(selector string, value string, opts goja.Value) @@ -51,8 +51,8 @@ type Page interface { Pause() Pdf(opts goja.Value) []byte Press(selector string, key string, opts goja.Value) - Query(selector string) ElementHandle - QueryAll(selector string) []ElementHandle + Query(selector string) (ElementHandle, error) + QueryAll(selector string) ([]ElementHandle, error) Reload(opts goja.Value) Response Route(url goja.Value, handler goja.Callable) Screenshot(opts goja.Value) goja.ArrayBuffer @@ -78,7 +78,7 @@ type Page interface { WaitForNavigation(opts goja.Value) (Response, error) WaitForRequest(urlOrPredicate, opts goja.Value) Request WaitForResponse(urlOrPredicate, opts goja.Value) Response - WaitForSelector(selector string, opts goja.Value) ElementHandle + WaitForSelector(selector string, opts goja.Value) (ElementHandle, error) WaitForTimeout(timeout int64) Workers() []Worker } diff --git a/vendor/github.com/grafana/xk6-browser/api/worker.go b/vendor/github.com/grafana/xk6-browser/api/worker.go index fee6ac0723e..92ece02131c 100644 --- a/vendor/github.com/grafana/xk6-browser/api/worker.go +++ b/vendor/github.com/grafana/xk6-browser/api/worker.go @@ -5,6 +5,6 @@ import "github.com/dop251/goja" // Worker is the interface of a web worker. type Worker interface { Evaluate(pageFunc goja.Value, args ...goja.Value) any - EvaluateHandle(pageFunc goja.Value, args ...goja.Value) JSHandle + EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (JSHandle, error) URL() string } diff --git a/vendor/github.com/grafana/xk6-browser/browser/mapping.go b/vendor/github.com/grafana/xk6-browser/browser/mapping.go index 7cc761d790e..d50dbe956b9 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/mapping.go +++ b/vendor/github.com/grafana/xk6-browser/browser/mapping.go @@ -2,36 +2,19 @@ package browser import ( "context" + "errors" "fmt" "github.com/dop251/goja" "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/chromium" + "github.com/grafana/xk6-browser/k6error" "github.com/grafana/xk6-browser/k6ext" k6common "go.k6.io/k6/js/common" - k6modules "go.k6.io/k6/js/modules" ) -// moduleVU carries module specific VU information. -// -// Currently, it is used to carry the VU object to the -// inner objects and promises. -type moduleVU struct { - k6modules.VU -} - -func (vu moduleVU) Context() context.Context { - // promises and inner objects need the VU object to be - // able to use k6-core specific functionality. - // - // We should not cache the context (especially the init - // context from the vu that is received from k6 in - // NewModuleInstance). - return k6ext.WithVU(vu.VU.Context(), vu.VU) -} - // mapping is a type of mapping between our API (api/) and the JS // module. It acts like a bridge and allows adding wildcard methods // and customization over our API. @@ -62,6 +45,41 @@ func mapBrowserToGoja(vu moduleVU) *goja.Object { return obj } +// mapLocator API to the JS module. +func mapLocator(vu moduleVU, lo api.Locator) mapping { + return mapping{ + "click": func(opts goja.Value) *goja.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + err := lo.Click(opts) + return nil, err //nolint:wrapcheck + }) + }, + "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": lo.Tap, + "dispatchEvent": lo.DispatchEvent, + "waitFor": lo.WaitFor, + } +} + // mapRequest to the JS module. func mapRequest(vu moduleVU, r api.Request) mapping { rt := vu.Runtime() @@ -119,7 +137,7 @@ func mapResponse(vu moduleVU, r api.Response) mapping { "headerValues": r.HeaderValues, "headers": r.Headers, "headersArray": r.HeadersArray, - "jSON": r.JSON, + "json": r.JSON, "ok": r.Ok, "request": func() *goja.Object { mr := mapRequest(vu, r.Request()) @@ -146,19 +164,24 @@ func mapJSHandle(vu moduleVU, jsh api.JSHandle) mapping { }, "dispose": jsh.Dispose, "evaluate": jsh.Evaluate, - "evaluateHandle": func(pageFunc goja.Value, args ...goja.Value) *goja.Object { - var ( - h = jsh.EvaluateHandle(pageFunc, args...) - m = mapJSHandle(vu, h) - ) - return rt.ToValue(m).ToObject(rt) + "evaluateHandle": func(pageFunc goja.Value, args ...goja.Value) (mapping, error) { + h, err := jsh.EvaluateHandle(pageFunc, args...) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapJSHandle(vu, h), nil }, - "getProperties": func() *goja.Object { + "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 jsh.GetProperties() { + for k, v := range props { dst[k] = mapJSHandle(vu, v) } - return rt.ToValue(dst).ToObject(rt) + return dst, nil }, "getProperty": func(propertyName string) *goja.Object { var ( @@ -168,7 +191,6 @@ func mapJSHandle(vu moduleVU, jsh api.JSHandle) mapping { return rt.ToValue(m).ToObject(rt) }, "jsonValue": jsh.JSONValue, - "objectID": jsh.ObjectID, } } @@ -176,7 +198,6 @@ func mapJSHandle(vu moduleVU, jsh api.JSHandle) mapping { // //nolint:funlen func mapElementHandle(vu moduleVU, eh api.ElementHandle) mapping { - rt := vu.Runtime() maps := mapping{ "boundingBox": eh.BoundingBox, "check": eh.Check, @@ -186,10 +207,12 @@ func mapElementHandle(vu moduleVU, eh api.ElementHandle) mapping { return nil, err //nolint:wrapcheck }) }, - "contentFrame": func() *goja.Object { - f := eh.ContentFrame() - mf := mapFrame(vu, f) - return rt.ToValue(mf).ToObject(rt) + "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": eh.DispatchEvent, @@ -206,10 +229,12 @@ func mapElementHandle(vu moduleVU, eh api.ElementHandle) mapping { "isEnabled": eh.IsEnabled, "isHidden": eh.IsHidden, "isVisible": eh.IsVisible, - "ownerFrame": func() *goja.Object { - f := eh.OwnerFrame() - mf := mapFrame(vu, f) - return rt.ToValue(mf).ToObject(rt) + "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": eh.Screenshot, @@ -222,27 +247,33 @@ func mapElementHandle(vu moduleVU, eh api.ElementHandle) mapping { "type": eh.Type, "uncheck": eh.Uncheck, "waitForElementState": eh.WaitForElementState, - "waitForSelector": func(selector string, opts goja.Value) *goja.Object { - eh := eh.WaitForSelector(selector, opts) - ehm := mapElementHandle(vu, eh) - return rt.ToValue(ehm).ToObject(rt) + "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) *goja.Object { - eh := eh.Query(selector) + maps["$"] = func(selector string) (mapping, error) { + eh, err := eh.Query(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } ehm := mapElementHandle(vu, eh) - return rt.ToValue(ehm).ToObject(rt) + return ehm, nil } - maps["$$"] = func(selector string) *goja.Object { - var ( - mehs []mapping - ehs = eh.QueryAll(selector) - ) + 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 rt.ToValue(mehs).ToObject(rt) + return mehs, nil } jsHandleMap := mapJSHandle(vu, eh) @@ -282,16 +313,21 @@ func mapFrame(vu moduleVU, f api.Frame) mapping { "dblclick": f.Dblclick, "dispatchEvent": f.DispatchEvent, "evaluate": f.Evaluate, - "evaluateHandle": func(pageFunction goja.Value, args ...goja.Value) *goja.Object { - jsh := f.EvaluateHandle(pageFunction, args...) - ehm := mapJSHandle(vu, jsh) - return rt.ToValue(ehm).ToObject(rt) + "evaluateHandle": func(pageFunction goja.Value, args ...goja.Value) (mapping, error) { + jsh, err := f.EvaluateHandle(pageFunction, args...) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapJSHandle(vu, jsh), nil }, "fill": f.Fill, "focus": f.Focus, - "frameElement": func() *goja.Object { - eh := mapElementHandle(vu, f.FrameElement()) - return rt.ToValue(eh).ToObject(rt) + "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 { @@ -315,10 +351,11 @@ func mapFrame(vu moduleVU, f api.Frame) mapping { "isEnabled": f.IsEnabled, "isHidden": f.IsHidden, "isVisible": f.IsVisible, - "iD": f.ID, - "loaderID": f.LoaderID, - "locator": f.Locator, - "name": f.Name, + "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) @@ -352,28 +389,34 @@ func mapFrame(vu moduleVU, f api.Frame) mapping { return mapResponse(vu, resp), nil }) }, - "waitForSelector": func(selector string, opts goja.Value) *goja.Object { - eh := f.WaitForSelector(selector, opts) - ehm := mapElementHandle(vu, eh) - return rt.ToValue(ehm).ToObject(rt) + "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) *goja.Object { - eh := f.Query(selector) + maps["$"] = func(selector string) (mapping, error) { + eh, err := f.Query(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } ehm := mapElementHandle(vu, eh) - return rt.ToValue(ehm).ToObject(rt) + return ehm, nil } - maps["$$"] = func(selector string) *goja.Object { - var ( - mehs []mapping - ehs = f.QueryAll(selector) - ) + 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 rt.ToValue(mehs).ToObject(rt) + return mehs, nil } return maps @@ -405,12 +448,12 @@ func mapPage(vu moduleVU, p api.Page) mapping { "emulateMedia": p.EmulateMedia, "emulateVisionDeficiency": p.EmulateVisionDeficiency, "evaluate": p.Evaluate, - "evaluateHandle": func(pageFunc goja.Value, args ...goja.Value) *goja.Object { - var ( - jsh = p.EvaluateHandle(pageFunc, args...) - m = mapJSHandle(vu, jsh) - ) - return rt.ToValue(m).ToObject(rt) + "evaluateHandle": func(pageFunc goja.Value, args ...goja.Value) (mapping, error) { + jsh, err := p.EvaluateHandle(pageFunc, args...) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapJSHandle(vu, jsh), nil }, "exposeBinding": p.ExposeBinding, "exposeFunction": p.ExposeFunction, @@ -428,8 +471,13 @@ func mapPage(vu moduleVU, p api.Page) mapping { return rt.ToValue(mfrs).ToObject(rt) }, "getAttribute": p.GetAttribute, - "goBack": p.GoBack, - "goForward": p.GoForward, + "goBack": func(opts goja.Value) *goja.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + resp := p.GoBack(opts) + return mapResponse(vu, resp), nil + }) + }, + "goForward": p.GoForward, "goto": func(url string, opts goja.Value) *goja.Promise { return k6ext.Promise(vu.Context(), func() (any, error) { resp, err := p.Goto(url, opts) @@ -452,7 +500,10 @@ func mapPage(vu moduleVU, p api.Page) mapping { "isHidden": p.IsHidden, "isVisible": p.IsVisible, "keyboard": rt.ToValue(p.GetKeyboard()).ToObject(rt), - "locator": p.Locator, + "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) @@ -503,8 +554,14 @@ func mapPage(vu moduleVU, p api.Page) mapping { }, "waitForRequest": p.WaitForRequest, "waitForResponse": p.WaitForResponse, - "waitForSelector": p.WaitForSelector, - "waitForTimeout": p.WaitForTimeout, + "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() { @@ -514,21 +571,25 @@ func mapPage(vu moduleVU, p api.Page) mapping { return rt.ToValue(mws).ToObject(rt) }, } - maps["$"] = func(selector string) *goja.Object { - eh := p.Query(selector) + maps["$"] = func(selector string) (mapping, error) { + eh, err := p.Query(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } ehm := mapElementHandle(vu, eh) - return rt.ToValue(ehm).ToObject(rt) + return ehm, nil } - maps["$$"] = func(selector string) *goja.Object { - var ( - mehs []mapping - ehs = p.QueryAll(selector) - ) + 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 rt.ToValue(mehs).ToObject(rt) + return mehs, nil } return maps @@ -536,13 +597,15 @@ func mapPage(vu moduleVU, p api.Page) mapping { // mapWorker to the JS module. func mapWorker(vu moduleVU, w api.Worker) mapping { - rt := vu.Runtime() return mapping{ "evaluate": w.Evaluate, - "evaluateHandle": func(pageFunc goja.Value, args ...goja.Value) *goja.Object { - h := w.EvaluateHandle(pageFunc, args...) - m := mapJSHandle(vu, h) - return rt.ToValue(m).ToObject(rt) + "evaluateHandle": func(pageFunc goja.Value, args ...goja.Value) (mapping, error) { + h, err := w.EvaluateHandle(pageFunc, args...) + if err != nil { + panicIfFatalError(vu.Context(), err) + return nil, err //nolint:wrapcheck + } + return mapJSHandle(vu, h), nil }, "url": w.URL(), } @@ -552,13 +615,18 @@ func mapWorker(vu moduleVU, w api.Worker) mapping { func mapBrowserContext(vu moduleVU, bc api.BrowserContext) mapping { rt := vu.Runtime() return mapping{ - "addCookies": bc.AddCookies, - "addInitScript": bc.AddInitScript, - "browser": bc.Browser, - "clearCookies": bc.ClearCookies, - "clearPermissions": bc.ClearPermissions, - "close": bc.Close, - "cookies": bc.Cookies, + "addCookies": bc.AddCookies, + "addInitScript": bc.AddInitScript, + "browser": bc.Browser, + "clearCookies": bc.ClearCookies, + "clearPermissions": bc.ClearPermissions, + "close": bc.Close, + "cookies": func() ([]any, error) { + cc, err := bc.Cookies() + ctx := vu.Context() + panicIfFatalError(ctx, err) + return cc, err //nolint:wrapcheck + }, "exposeBinding": bc.ExposeBinding, "exposeFunction": bc.ExposeFunction, "grantPermissions": bc.GrantPermissions, @@ -566,13 +634,20 @@ func mapBrowserContext(vu moduleVU, bc api.BrowserContext) mapping { "route": bc.Route, "setDefaultNavigationTimeout": bc.SetDefaultNavigationTimeout, "setDefaultTimeout": bc.SetDefaultTimeout, - "setExtraHTTPHeaders": bc.SetExtraHTTPHeaders, - "setGeolocation": bc.SetGeolocation, - "setHTTPCredentials": bc.SetHTTPCredentials, //nolint:staticcheck - "setOffline": bc.SetOffline, - "storageState": bc.StorageState, - "unroute": bc.Unroute, - "waitForEvent": bc.WaitForEvent, + "setExtraHTTPHeaders": func(headers map[string]string) *goja.Promise { + ctx := vu.Context() + return k6ext.Promise(ctx, func() (result any, reason error) { + err := bc.SetExtraHTTPHeaders(headers) + panicIfFatalError(ctx, err) + return nil, err //nolint:wrapcheck + }) + }, + "setGeolocation": bc.SetGeolocation, + "setHTTPCredentials": bc.SetHTTPCredentials, //nolint:staticcheck + "setOffline": bc.SetOffline, + "storageState": bc.StorageState, + "unroute": bc.Unroute, + "waitForEvent": bc.WaitForEvent, "pages": func() *goja.Object { var ( mpages []mapping @@ -588,10 +663,12 @@ func mapBrowserContext(vu moduleVU, bc api.BrowserContext) mapping { return rt.ToValue(mpages).ToObject(rt) }, - "newPage": func() *goja.Object { - page := bc.NewPage() - m := mapPage(vu, page) - return rt.ToValue(m).ToObject(rt) + "newPage": func() (mapping, error) { + page, err := bc.NewPage() + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapPage(vu, page), nil }, } } @@ -610,15 +687,20 @@ func mapBrowser(vu moduleVU, b api.Browser) mapping { }, "userAgent": b.UserAgent, "version": b.Version, - "newContext": func(opts goja.Value) *goja.Object { - bctx := b.NewContext(opts) + "newContext": func(opts goja.Value) (*goja.Object, error) { + bctx, err := b.NewContext(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } m := mapBrowserContext(vu, bctx) - return rt.ToValue(m).ToObject(rt) + return rt.ToValue(m).ToObject(rt), nil }, - "newPage": func(opts goja.Value) *goja.Object { - page := b.NewPage(opts) - m := mapPage(vu, page) - return rt.ToValue(m).ToObject(rt) + "newPage": func(opts goja.Value) (mapping, error) { + page, err := b.NewPage(opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + return mapPage(vu, page), nil }, } } @@ -627,13 +709,26 @@ func mapBrowser(vu moduleVU, b api.Browser) mapping { func mapBrowserType(vu moduleVU, bt api.BrowserType) mapping { rt := vu.Runtime() return mapping{ - "connect": bt.Connect, + "connect": func(wsEndpoint string, opts goja.Value) *goja.Object { + b := bt.Connect(wsEndpoint, opts) + m := mapBrowser(vu, b) + return rt.ToValue(m).ToObject(rt) + }, "executablePath": bt.ExecutablePath, "launchPersistentContext": bt.LaunchPersistentContext, "name": bt.Name, "launch": func(opts goja.Value) *goja.Object { - m := mapBrowser(vu, bt.Launch(opts)) + b, pid := bt.Launch(opts) + // store the pid so we can kill it later on panic. + vu.registerPid(pid) + m := mapBrowser(vu, b) return rt.ToValue(m).ToObject(rt) }, } } + +func panicIfFatalError(ctx context.Context, err error) { + if errors.Is(err, k6error.ErrFatal) { + k6ext.Abort(ctx, err.Error()) + } +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/module.go b/vendor/github.com/grafana/xk6-browser/browser/module.go index 1b0082f640c..bdc28ef380d 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/module.go +++ b/vendor/github.com/grafana/xk6-browser/browser/module.go @@ -9,12 +9,14 @@ import ( k6modules "go.k6.io/k6/js/modules" ) -const version = "0.8.0" +const version = "0.8.1" type ( // RootModule is the global module instance that will create module // instances for each VU. - RootModule struct{} + RootModule struct { + pidRegistry *pidRegistry + } // JSModule exposes the properties available to the JS script. JSModule struct { @@ -36,17 +38,22 @@ var ( // New returns a pointer to a new RootModule instance. func New() *RootModule { - return &RootModule{} + return &RootModule{ + pidRegistry: &pidRegistry{}, + } } // NewModuleInstance implements the k6modules.Module interface to return // a new instance for each VU. -func (*RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance { +func (m *RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance { return &ModuleInstance{ mod: &JSModule{ - Chromium: mapBrowserToGoja(moduleVU{vu}), - Devices: common.GetDevices(), - Version: version, + Chromium: mapBrowserToGoja(moduleVU{ + VU: vu, + pidRegistry: m.pidRegistry, + }), + Devices: common.GetDevices(), + Version: version, }, } } diff --git a/vendor/github.com/grafana/xk6-browser/browser/modulevu.go b/vendor/github.com/grafana/xk6-browser/browser/modulevu.go new file mode 100644 index 00000000000..3c38d2aa39c --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/modulevu.go @@ -0,0 +1,29 @@ +package browser + +import ( + "context" + + "github.com/grafana/xk6-browser/k6ext" + + k6modules "go.k6.io/k6/js/modules" +) + +// moduleVU carries module specific VU information. +// +// Currently, it is used to carry the VU object to the inner objects and +// promises. +type moduleVU struct { + k6modules.VU + + *pidRegistry +} + +func (vu moduleVU) Context() context.Context { + // promises and inner objects need the VU object to be + // able to use k6-core specific functionality. + // + // We should not cache the context (especially the init + // context from the vu that is received from k6 in + // NewModuleInstance). + return k6ext.WithVU(vu.VU.Context(), vu) +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/pidregistry.go b/vendor/github.com/grafana/xk6-browser/browser/pidregistry.go new file mode 100644 index 00000000000..a505c53d0d0 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/pidregistry.go @@ -0,0 +1,28 @@ +package browser + +import "sync" + +// pidRegistry keeps track of the launched browser process IDs. +type pidRegistry struct { + mu sync.RWMutex + ids []int +} + +// registerPid registers the launched browser process ID. +func (r *pidRegistry) registerPid(pid int) { + r.mu.Lock() + defer r.mu.Unlock() + + r.ids = append(r.ids, pid) +} + +// Pids returns the launched browser process IDs. +func (r *pidRegistry) Pids() []int { + r.mu.RLock() + defer r.mu.RUnlock() + + pids := make([]int, len(r.ids)) + copy(pids, r.ids) + + return pids +} 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 fd0091d0cb7..ba6b9fa9768 100644 --- a/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go +++ b/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go @@ -9,7 +9,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "sort" "strings" "time" @@ -39,10 +38,8 @@ type BrowserType struct { vu k6modules.VU hooks *common.Hooks k6Metrics *k6ext.CustomMetrics - execPath string // path to the Chromium executable - storage *storage.Dir // stores temporary data for the extension and user + execPath string // path to the Chromium executable randSrc *rand.Rand - logger *log.Logger } // NewBrowserType registers our custom k6 metrics, creates method mappings on @@ -55,57 +52,42 @@ func NewBrowserType(vu k6modules.VU) api.BrowserType { vu: vu, hooks: common.NewHooks(), k6Metrics: k6m, - storage: &storage.Dir{}, randSrc: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint: gosec } return &b } -// Connect attaches k6 browser to an existing browser instance. -func (b *BrowserType) Connect(opts goja.Value) { - rt := b.vu.Runtime() - k6common.Throw(rt, errors.New("BrowserType.connect() has not been implemented yet")) -} +func (b *BrowserType) init( + opts goja.Value, isRemoteBrowser bool, +) (context.Context, *common.LaunchOptions, *log.Logger, error) { + ctx := b.initContext() -// ExecutablePath returns the path where the extension expects to find the browser executable. -func (b *BrowserType) ExecutablePath() (execPath string) { - if b.execPath != "" { - return b.execPath + logger, err := makeLogger(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("error setting up logger: %w", err) } - defer func() { - b.execPath = execPath - }() - for _, path := range [...]string{ - // Unix-like - "headless_shell", - "headless-shell", - "chromium", - "chromium-browser", - "google-chrome", - "google-chrome-stable", - "google-chrome-beta", - "google-chrome-unstable", - "/usr/bin/google-chrome", + var launchOpts *common.LaunchOptions + if isRemoteBrowser { + launchOpts = common.NewRemoteBrowserLaunchOptions() + } else { + launchOpts = common.NewLaunchOptions() + } - // Windows - "chrome", - "chrome.exe", // in case PATHEXT is misconfigured - `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, - `C:\Program Files\Google\Chrome\Application\chrome.exe`, - filepath.Join(os.Getenv("USERPROFILE"), `AppData\Local\Google\Chrome\Application\chrome.exe`), + if err = launchOpts.Parse(ctx, logger, opts); err != nil { + return nil, nil, nil, fmt.Errorf("error parsing launch options: %w", err) + } + ctx = common.WithLaunchOptions(ctx, launchOpts) - // Mac (from https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Mac/857950/) - "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - "/Applications/Chromium.app/Contents/MacOS/Chromium", - } { - if _, err := exec.LookPath(path); err == nil { - return path - } + if err := logger.SetCategoryFilter(launchOpts.LogCategoryFilter); err != nil { + return nil, nil, nil, fmt.Errorf("error setting category filter: %w", err) + } + if launchOpts.Debug { + _ = logger.SetLevel("debug") } - return "" + return ctx, launchOpts, logger, nil } func (b *BrowserType) initContext() context.Context { @@ -116,22 +98,14 @@ func (b *BrowserType) initContext() context.Context { return ctx } -// Launch allocates a new Chrome browser process and returns a new api.Browser value, -// which can be used for controlling the Chrome browser. -func (b *BrowserType) Launch(opts goja.Value) api.Browser { - ctx := b.initContext() - - var err error - if b.logger, err = makeLogger(ctx); err != nil { - k6ext.Panic(ctx, "setting up logger: %w", err) - } - launchOpts := common.NewLaunchOptions(k6ext.OnCloud()) - if err := launchOpts.Parse(ctx, b.logger, opts); err != nil { - k6ext.Panic(ctx, "parsing launch options: %w", err) +// Connect attaches k6 browser to an existing browser instance. +func (b *BrowserType) Connect(wsEndpoint string, opts goja.Value) api.Browser { + ctx, launchOpts, logger, err := b.init(opts, true) + if err != nil { + k6ext.Panic(ctx, "initializing browser type: %w", err) } - ctx = common.WithLaunchOptions(ctx, launchOpts) - bp, err := b.launch(ctx, launchOpts) + bp, err := b.connect(ctx, wsEndpoint, launchOpts, logger) if err != nil { err = &k6ext.UserFriendlyError{ Err: err, @@ -143,32 +117,83 @@ func (b *BrowserType) Launch(opts goja.Value) api.Browser { return bp } -func (b *BrowserType) launch(ctx context.Context, opts *common.LaunchOptions) (*common.Browser, error) { - if err := b.logger.SetCategoryFilter(opts.LogCategoryFilter); err != nil { - return nil, fmt.Errorf("%w", err) +func (b *BrowserType) connect( + ctx context.Context, wsURL string, opts *common.LaunchOptions, logger *log.Logger, +) (*common.Browser, error) { + browserProc, err := b.link(ctx, wsURL, opts, logger) + if browserProc == nil { + return nil, fmt.Errorf("connecting to browser: %w", err) } - if opts.Debug { - _ = b.logger.SetLevel("debug") + + // If this context is cancelled we'll initiate an extension wide + // cancellation and shutdown. + browserCtx, browserCtxCancel := context.WithCancel(ctx) + b.Ctx = browserCtx + browser, err := common.NewBrowser( + browserCtx, browserCtxCancel, browserProc, opts, logger, + ) + if err != nil { + return nil, fmt.Errorf("connecting to browser: %w", err) } + return browser, nil +} + +func (b *BrowserType) link( + ctx context.Context, wsURL string, + opts *common.LaunchOptions, logger *log.Logger, +) (*common.BrowserProcess, error) { + bProcCtx, bProcCtxCancel := context.WithTimeout(ctx, opts.Timeout) + p, err := common.NewRemoteBrowserProcess(bProcCtx, wsURL, bProcCtxCancel, logger) + if err != nil { + bProcCtxCancel() + return nil, err //nolint:wrapcheck + } + + return p, nil +} + +// Launch allocates a new Chrome browser process and returns a new api.Browser value, +// which can be used for controlling the Chrome browser. +func (b *BrowserType) Launch(opts goja.Value) (_ api.Browser, browserProcessID int) { + ctx, launchOpts, logger, err := b.init(opts, false) + if err != nil { + k6ext.Panic(ctx, "initializing browser type: %w", err) + } + + bp, pid, err := b.launch(ctx, launchOpts, logger) + if err != nil { + err = &k6ext.UserFriendlyError{ + Err: err, + Timeout: launchOpts.Timeout, + } + k6ext.Panic(ctx, "%w", err) + } + + return bp, pid +} + +func (b *BrowserType) launch( + ctx context.Context, opts *common.LaunchOptions, logger *log.Logger, +) (_ *common.Browser, pid int, _ error) { envs := make([]string, 0, len(opts.Env)) for k, v := range opts.Env { envs = append(envs, fmt.Sprintf("%s=%s", k, v)) } flags, err := prepareFlags(opts, &(b.vu.State()).Options) if err != nil { - return nil, fmt.Errorf("%w", err) + return nil, 0, fmt.Errorf("%w", err) } - dataDir := b.storage + dataDir := &storage.Dir{} if err := dataDir.Make("", flags["user-data-dir"]); err != nil { - return nil, fmt.Errorf("%w", err) + return nil, 0, fmt.Errorf("%w", err) } flags["user-data-dir"] = dataDir.Dir go func(c context.Context) { defer func() { if err := dataDir.Cleanup(); err != nil { - b.logger.Errorf("BrowserType:Launch", "cleaning up the user data directory: %v", err) + logger.Errorf("BrowserType:Launch", "cleaning up the user data directory: %v", err) } }() // There's a small chance that this might be called @@ -179,28 +204,22 @@ func (b *BrowserType) launch(ctx context.Context, opts *common.LaunchOptions) (* <-c.Done() }(ctx) - browserProc, err := b.allocate(ctx, opts, flags, envs, dataDir, b.logger) + browserProc, err := b.allocate(ctx, opts, flags, envs, dataDir, logger) if browserProc == nil { - return nil, fmt.Errorf("launching browser: %w", err) + return nil, 0, fmt.Errorf("launching browser: %w", err) } - browserProc.AttachLogger(b.logger) - // If this context is cancelled we'll initiate an extension wide // cancellation and shutdown. browserCtx, browserCtxCancel := context.WithCancel(ctx) - // attach the browser process ID to the context - // so that we can kill it afterward if it lingers - // see: k6ext.Panic function. - browserCtx = k6ext.WithProcessID(browserCtx, browserProc.Pid()) b.Ctx = browserCtx browser, err := common.NewBrowser(browserCtx, browserCtxCancel, - browserProc, opts, b.logger) + browserProc, opts, logger) if err != nil { - return nil, fmt.Errorf("launching browser: %w", err) + return nil, 0, fmt.Errorf("launching browser: %w", err) } - return browser, nil + return browser, browserProc.Pid(), nil } // LaunchPersistentContext launches the browser with persistent storage. @@ -238,7 +257,47 @@ func (b *BrowserType) allocate( path = b.ExecutablePath() } - return common.NewBrowserProcess(bProcCtx, path, args, env, dataDir, bProcCtxCancel, logger) //nolint: wrapcheck + return common.NewLocalBrowserProcess(bProcCtx, path, args, env, dataDir, bProcCtxCancel, logger) //nolint: wrapcheck +} + +// ExecutablePath returns the path where the extension expects to find the browser executable. +func (b *BrowserType) ExecutablePath() (execPath string) { + if b.execPath != "" { + return b.execPath + } + defer func() { + b.execPath = execPath + }() + + for _, path := range [...]string{ + // Unix-like + "headless_shell", + "headless-shell", + "chromium", + "chromium-browser", + "google-chrome", + "google-chrome-stable", + "google-chrome-beta", + "google-chrome-unstable", + "/usr/bin/google-chrome", + + // Windows + "chrome", + "chrome.exe", // in case PATHEXT is misconfigured + `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, + `C:\Program Files\Google\Chrome\Application\chrome.exe`, + filepath.Join(os.Getenv("USERPROFILE"), `AppData\Local\Google\Chrome\Application\chrome.exe`), + + // Mac (from https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Mac/857950/) + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + } { + if _, err := exec.LookPath(path); err == nil { + return path + } + } + + return "" } // parseArgs parses command-line arguments and returns them. @@ -257,12 +316,6 @@ func parseArgs(flags map[string]any) ([]string, error) { return nil, fmt.Errorf(`invalid browser command line flag: "%s=%v"`, name, value) } } - if _, ok := flags["no-sandbox"]; !ok && os.Getuid() == 0 { - // Running as root, for example in a Linux container. Chromium - // needs --no-sandbox when running as root, so make that the - // default, unless the user set "no-sandbox": false. - args = append(args, "--no-sandbox") - } if _, ok := flags["remote-debugging-port"]; !ok { args = append(args, "--remote-debugging-port=0") } @@ -282,38 +335,31 @@ func prepareFlags(lopts *common.LaunchOptions, k6opts *k6lib.Options) (map[strin "disable-background-timer-throttling": true, "disable-backgrounding-occluded-windows": true, "disable-breakpad": true, - "disable-client-side-phishing-detection": true, "disable-component-extensions-with-background-pages": true, "disable-default-apps": true, "disable-dev-shm-usage": true, "disable-extensions": true, //nolint:lll - "disable-features": "ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,AcceptCHFrame", - "disable-hang-monitor": true, - "disable-ipc-flooding-protection": true, - "disable-popup-blocking": true, - "disable-prompt-on-repost": true, - "disable-renderer-backgrounding": true, - "disable-sync": true, - "force-color-profile": "srgb", - "metrics-recording-only": true, - "no-first-run": true, - "safebrowsing-disable-auto-update": true, - "enable-automation": true, - "password-store": "basic", - "use-mock-keychain": true, - "no-service-autorun": true, + "disable-features": "ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,AcceptCHFrame", + "disable-hang-monitor": true, + "disable-ipc-flooding-protection": true, + "disable-popup-blocking": true, + "disable-prompt-on-repost": true, + "disable-renderer-backgrounding": true, + "force-color-profile": "srgb", + "metrics-recording-only": true, + "no-first-run": true, + "enable-automation": true, + "password-store": "basic", + "use-mock-keychain": true, + "no-service-autorun": true, "no-startup-window": true, "no-default-browser-check": true, - "no-sandbox": true, "headless": lopts.Headless, "auto-open-devtools-for-tabs": lopts.Devtools, "window-size": fmt.Sprintf("%d,%d", 800, 600), } - if runtime.GOOS == "darwin" { - f["enable-use-zoom-for-dsf"] = false - } if lopts.Headless { f["hide-scrollbars"] = true f["mute-audio"] = true diff --git a/vendor/github.com/grafana/xk6-browser/common/browser.go b/vendor/github.com/grafana/xk6-browser/common/browser.go index 17ab2e09b48..93ed8a77f89 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser.go @@ -2,6 +2,7 @@ package common import ( "context" + "errors" "fmt" "strings" "sync" @@ -23,8 +24,10 @@ import ( ) // Ensure Browser implements the EventEmitter and Browser interfaces. -var _ EventEmitter = &Browser{} -var _ api.Browser = &Browser{} +var ( + _ EventEmitter = &Browser{} + _ api.Browser = &Browser{} +) const ( BrowserStateOpen int64 = iota @@ -62,6 +65,9 @@ type Browser struct { sessionIDtoTargetIDMu sync.RWMutex sessionIDtoTargetID map[target.SessionID]target.ID + // Used to display a warning when the browser is reclosed. + closed bool + vu k6modules.VU logger *log.Logger @@ -115,7 +121,10 @@ func (b *Browser) connect() error { b.conn = conn // We don't need to lock this because `connect()` is called only in NewBrowser - b.defaultContext = NewBrowserContext(b.ctx, b, "", NewBrowserContextOptions(), b.logger) + b.defaultContext, err = NewBrowserContext(b.ctx, b, "", NewBrowserContextOptions(), b.logger) + if err != nil { + return fmt.Errorf("browser connect: %w", err) + } return b.initEvents() } @@ -391,8 +400,17 @@ func (b *Browser) newPageInContext(id cdp.BrowserContextID) (*Page, error) { // Close shuts down the browser. func (b *Browser) Close() { + if b.closed { + b.logger.Warnf( + "Browser:Close", + "Please call browser.close only once, and do not use the browser after calling close.", + ) + return + } + b.closed = true + defer func() { - if err := b.browserProc.userDataDir.Cleanup(); err != nil { + if err := b.browserProc.Cleanup(); err != nil { b.logger.Errorf("Browser:Close", "cleaning up the user data directory: %v", err) } }() @@ -408,11 +426,12 @@ func (b *Browser) Close() { b.conn.IgnoreIOErrors() b.browserProc.GracefulClose() - // Send the Browser.close CDP command, which triggers the browser process to - // exit. - action := cdpbrowser.Close() - if err := action.Do(cdp.WithExecutor(b.ctx, b.conn)); err != nil { - if _, ok := err.(*websocket.CloseError); !ok { + // If the browser is not being executed remotely, send the Browser.close CDP + // command, which triggers the browser process to exit. + if !b.launchOpts.isRemoteBrowser { + var closeErr *websocket.CloseError + err := cdpbrowser.Close().Do(cdp.WithExecutor(b.ctx, b.conn)) + if err != nil && !errors.As(err, &closeErr) { k6ext.Panic(b.ctx, "closing the browser: %v", err) } } @@ -459,7 +478,7 @@ func (b *Browser) IsConnected() bool { } // NewContext creates a new incognito-like browser context. -func (b *Browser) NewContext(opts goja.Value) api.BrowserContext { +func (b *Browser) NewContext(opts goja.Value) (api.BrowserContext, error) { action := target.CreateBrowserContext().WithDisposeOnDetach(true) browserContextID, err := action.Do(cdp.WithExecutor(b.ctx, b.conn)) b.logger.Debugf("Browser:NewContext", "bctxid:%v", browserContextID) @@ -474,15 +493,22 @@ func (b *Browser) NewContext(opts goja.Value) api.BrowserContext { b.contextsMu.Lock() defer b.contextsMu.Unlock() - browserCtx := NewBrowserContext(b.ctx, b, browserContextID, browserCtxOpts, b.logger) + browserCtx, err := NewBrowserContext(b.ctx, b, browserContextID, browserCtxOpts, b.logger) + if err != nil { + return nil, fmt.Errorf("new context: %w", err) + } b.contexts[browserContextID] = browserCtx - return browserCtx + return browserCtx, nil } // NewPage creates a new tab in the browser window. -func (b *Browser) NewPage(opts goja.Value) api.Page { - browserCtx := b.NewContext(opts) +func (b *Browser) NewPage(opts goja.Value) (api.Page, error) { + browserCtx, err := b.NewContext(opts) + if err != nil { + return nil, fmt.Errorf("new page: %w", err) + } + return browserCtx.NewPage() } @@ -524,3 +550,8 @@ func (b *Browser) Version() string { } return product[i+1:] } + +// 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 6aac1424538..b98e8a6daca 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser_context.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser_context.go @@ -7,6 +7,8 @@ import ( "time" "github.com/grafana/xk6-browser/api" + "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" @@ -14,14 +16,17 @@ import ( 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" ) // Ensure BrowserContext implements the EventEmitter and api.BrowserContext interfaces. -var _ EventEmitter = &BrowserContext{} -var _ api.BrowserContext = &BrowserContext{} +var ( + _ EventEmitter = &BrowserContext{} + _ api.BrowserContext = &BrowserContext{} +) // BrowserContext stores context information for a single independent browser session. // A newly launched browser instance contains a default browser context. @@ -44,7 +49,7 @@ type BrowserContext struct { // NewBrowserContext creates a new browser context. func NewBrowserContext( ctx context.Context, browser *Browser, id cdp.BrowserContextID, opts *BrowserContextOptions, logger *log.Logger, -) *BrowserContext { +) (*BrowserContext, error) { b := BrowserContext{ BaseEventEmitter: NewBaseEventEmitter(ctx), ctx: ctx, @@ -60,22 +65,39 @@ func NewBrowserContext( b.GrantPermissions(opts.Permissions, nil) } - return &b + rt := b.vu.Runtime() + wv := rt.ToValue(js.WebVitalIIFEScript) + wvi := rt.ToValue(js.WebVitalInitScript) + + if err := b.AddInitScript(wv, nil); err != nil { + return nil, fmt.Errorf("adding web vital script to new browser context: %w", err) + } + if err := b.AddInitScript(wvi, nil); err != nil { + return nil, fmt.Errorf("adding web vital init script to new browser context: %w", err) + } + + return &b, nil } -// AddCookies is not implemented. +// AddCookies adds cookies into this browser context. +// All pages within this context will have these cookies installed. func (b *BrowserContext) AddCookies(cookies goja.Value) { - k6ext.Panic(b.ctx, "BrowserContext.addCookies(cookies) has not been implemented yet") + b.logger.Debugf("BrowserContext:AddCookies", "bctxid:%v", b.id) + + err := b.addCookies(cookies) + if err != nil { + k6ext.Panic(b.ctx, "adding cookies: %w", err) + } } // AddInitScript adds a script that will be initialized on all new pages. -func (b *BrowserContext) AddInitScript(script goja.Value, arg goja.Value) { +func (b *BrowserContext) AddInitScript(script goja.Value, arg goja.Value) error { b.logger.Debugf("BrowserContext:AddInitScript", "bctxid:%v", b.id) rt := b.vu.Runtime() source := "" - if script != nil && !goja.IsUndefined(script) && !goja.IsNull(script) { + if gojaValueExists(script) { switch script.ExportType() { case reflect.TypeOf(string("")): source = script.String() @@ -100,8 +122,22 @@ func (b *BrowserContext) AddInitScript(script goja.Value, arg goja.Value) { b.evaluateOnNewDocumentSources = append(b.evaluateOnNewDocumentSources, source) for _, p := range b.browser.getPages() { - p.evaluateOnNewDocument(source) + if err := p.evaluateOnNewDocument(source); err != nil { + return fmt.Errorf("adding init script to browser context: %w", err) + } } + + return nil +} + +func (b *BrowserContext) applyAllInitScripts(p *Page) error { + for _, source := range b.evaluateOnNewDocumentSources { + if err := p.evaluateOnNewDocument(source); err != nil { + return fmt.Errorf("adding init script to browser context: %w", err) + } + } + + return nil } // Browser returns the browser instance that this browser context belongs to. @@ -142,9 +178,8 @@ func (b *BrowserContext) Close() { } // Cookies is not implemented. -func (b *BrowserContext) Cookies() []any { - k6ext.Panic(b.ctx, "BrowserContext.cookies() has not been implemented yet") - return nil +func (b *BrowserContext) Cookies() ([]any, error) { + return nil, fmt.Errorf("BrowserContext.cookies() has not been implemented yet: %w", k6error.ErrFatal) } // ExposeBinding is not implemented. @@ -209,12 +244,12 @@ func (b *BrowserContext) NewCDPSession() api.CDPSession { } // NewPage creates a new page inside this browser context. -func (b *BrowserContext) NewPage() api.Page { +func (b *BrowserContext) NewPage() (api.Page, error) { b.logger.Debugf("BrowserContext:NewPage", "bctxid:%v", b.id) p, err := b.browser.newPageInContext(b.id) if err != nil { - k6ext.Panic(b.ctx, "newPageInContext: %w", err) + return nil, fmt.Errorf("creating new page in browser context: %w", err) } var ( @@ -229,7 +264,7 @@ func (b *BrowserContext) NewPage() api.Page { } b.logger.Debugf("BrowserContext:NewPage:return", "bctxid:%v ptid:%s", bctxid, ptid) - return p + return p, nil } // Pages returns a list of pages inside this browser context. @@ -261,8 +296,8 @@ func (b *BrowserContext) SetDefaultTimeout(timeout int64) { } // SetExtraHTTPHeaders is not implemented. -func (b *BrowserContext) SetExtraHTTPHeaders(headers map[string]string) { - k6ext.Panic(b.ctx, "BrowserContext.setExtraHTTPHeaders(headers) has not been implemented yet") +func (b *BrowserContext) SetExtraHTTPHeaders(headers map[string]string) error { + return fmt.Errorf("BrowserContext.setExtraHTTPHeaders(headers) has not been implemented yet: %w", k6error.ErrFatal) } // SetGeolocation overrides the geo location of the user. @@ -427,3 +462,49 @@ func (b *BrowserContext) runWaitForEventHandler( func (b *BrowserContext) getSession(id target.SessionID) *Session { return b.browser.conn.getSession(id) } + +func (b *BrowserContext) addCookies(cookies goja.Value) error { + var cookieParams []network.CookieParam + if !gojaValueExists(cookies) { + return Error("cookies value is not set") + } + + rt := b.vu.Runtime() + err := rt.ExportTo(cookies, &cookieParams) + if err != nil { + return fmt.Errorf("unable to export cookies value to cookieParams. %w", err) + } + + // Create new array of pointers to items in cookieParams + var cookieParamsPointers []*network.CookieParam + for i := 0; i < len(cookieParams); i++ { + cookieParam := cookieParams[i] + + if cookieParam.Name == "" { + return fmt.Errorf("cookie name is not set. %#v", cookieParam) + } + + if cookieParam.Value == "" { + return fmt.Errorf("cookie value is not set. %#v", cookieParam) + } + + // if URL is not set, both Domain and Path must be provided + if cookieParam.URL == "" { + if cookieParam.Domain == "" || cookieParam.Path == "" { + return fmt.Errorf( + "if cookie url is not provided, both domain and path must be specified. %#v", + cookieParam, + ) + } + } + + cookieParamsPointers = append(cookieParamsPointers, &cookieParam) + } + + action := storage.SetCookies(cookieParamsPointers).WithBrowserContextID(b.id) + if err := action.Do(cdp.WithExecutor(b.ctx, b.browser.conn)); err != nil { + return fmt.Errorf("unable to execute SetCookies action: %w", err) + } + + return nil +} diff --git a/vendor/github.com/grafana/xk6-browser/common/browser_options.go b/vendor/github.com/grafana/xk6-browser/common/browser_options.go index c6121cfa2db..0732090c853 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser_options.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser_options.go @@ -10,6 +10,20 @@ import ( "github.com/grafana/xk6-browser/log" ) +const ( + optArgs = "args" + optDebug = "debug" + optDevTools = "devtools" + optEnv = "env" + optExecutablePath = "executablePath" + optHeadless = "headless" + optIgnoreDefaultArgs = "ignoreDefaultArgs" + optLogCategoryFilter = "logCategoryFilter" + optProxy = "proxy" + optSlowMo = "slowMo" + optTimeout = "timeout" +) + // ProxyOptions allows configuring a proxy server. type ProxyOptions struct { Server string @@ -32,7 +46,7 @@ type LaunchOptions struct { SlowMo time.Duration Timeout time.Duration - onCloud bool // some options will be ignored when running in the cloud + isRemoteBrowser bool // some options will be ignored if browser is in a remote machine } // LaunchPersistentContextOptions stores browser launch options for persistent context. @@ -42,13 +56,24 @@ type LaunchPersistentContextOptions struct { } // NewLaunchOptions returns a new LaunchOptions. -func NewLaunchOptions(onCloud bool) *LaunchOptions { +func NewLaunchOptions() *LaunchOptions { return &LaunchOptions{ Env: make(map[string]string), Headless: true, LogCategoryFilter: ".*", Timeout: DefaultTimeout, - onCloud: onCloud, + } +} + +// NewRemoteBrowserLaunchOptions returns a new LaunchOptions +// for a browser running in a remote machine. +func NewRemoteBrowserLaunchOptions() *LaunchOptions { + return &LaunchOptions{ + Env: make(map[string]string), + Headless: true, + LogCategoryFilter: ".*", + Timeout: DefaultTimeout, + isRemoteBrowser: true, } } @@ -62,15 +87,15 @@ func (l *LaunchOptions) Parse(ctx context.Context, logger *log.Logger, opts goja rt = k6ext.Runtime(ctx) o = opts.ToObject(rt) defaults = map[string]any{ - "env": l.Env, - "headless": l.Headless, - "logCategoryFilter": l.LogCategoryFilter, - "timeout": l.Timeout, + optEnv: l.Env, + optHeadless: l.Headless, + optLogCategoryFilter: l.LogCategoryFilter, + optTimeout: l.Timeout, } ) for _, k := range o.Keys() { - if l.shouldIgnoreOnCloud(k) { - logger.Warnf("LaunchOptions", "setting %s option is disallowed on cloud.", k) + if l.shouldIgnoreIfBrowserIsRemote(k) { + logger.Warnf("LaunchOptions", "setting %s option is disallowed when browser is remote", k) continue } v := o.Get(k) @@ -82,27 +107,27 @@ func (l *LaunchOptions) Parse(ctx context.Context, logger *log.Logger, opts goja } var err error switch k { - case "args": + case optArgs: err = exportOpt(rt, k, v, &l.Args) - case "debug": + case optDebug: l.Debug, err = parseBoolOpt(k, v) - case "devtools": + case optDevTools: l.Devtools, err = parseBoolOpt(k, v) - case "env": + case optEnv: err = exportOpt(rt, k, v, &l.Env) - case "executablePath": + case optExecutablePath: l.ExecutablePath, err = parseStrOpt(k, v) - case "headless": + case optHeadless: l.Headless, err = parseBoolOpt(k, v) - case "ignoreDefaultArgs": + case optIgnoreDefaultArgs: err = exportOpt(rt, k, v, &l.IgnoreDefaultArgs) - case "logCategoryFilter": + case optLogCategoryFilter: l.LogCategoryFilter, err = parseStrOpt(k, v) - case "proxy": + case optProxy: err = exportOpt(rt, k, v, &l.Proxy) - case "slowMo": + case optSlowMo: l.SlowMo, err = parseTimeOpt(k, v) - case "timeout": + case optTimeout: l.Timeout, err = parseTimeOpt(k, v) } if err != nil { @@ -113,9 +138,21 @@ func (l *LaunchOptions) Parse(ctx context.Context, logger *log.Logger, opts goja return nil } -func (l *LaunchOptions) shouldIgnoreOnCloud(opt string) bool { - if !l.onCloud { +func (l *LaunchOptions) shouldIgnoreIfBrowserIsRemote(opt string) bool { + if !l.isRemoteBrowser { return false } - return opt == "devtools" || opt == "executablePath" || opt == "headless" + + shouldIgnoreIfBrowserIsRemote := map[string]struct{}{ + optArgs: {}, + optDevTools: {}, + optEnv: {}, + optExecutablePath: {}, + optHeadless: {}, + optIgnoreDefaultArgs: {}, + optProxy: {}, + } + _, ignore := shouldIgnoreIfBrowserIsRemote[opt] + + return ignore } diff --git a/vendor/github.com/grafana/xk6-browser/common/browser_process.go b/vendor/github.com/grafana/xk6-browser/common/browser_process.go index df9d83f2331..47e0862aba3 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser_process.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser_process.go @@ -19,8 +19,7 @@ type BrowserProcess struct { ctx context.Context cancel context.CancelFunc - // The process of the browser, if running locally. - process *os.Process + meta browserProcessMeta // Channels for managing termination. lostConnection chan struct{} @@ -30,13 +29,12 @@ type BrowserProcess struct { // Browser's WebSocket URL to speak CDP wsURL string - // The directory where user data for the browser is stored. - userDataDir *storage.Dir - logger *log.Logger } -func NewBrowserProcess( +// NewLocalBrowserProcess starts a local browser process and +// returns a new BrowserProcess instance to interact with it. +func NewLocalBrowserProcess( ctx context.Context, path string, args, env []string, dataDir *storage.Dir, ctxCancel context.CancelFunc, logger *log.Logger, ) (*BrowserProcess, error) { @@ -50,35 +48,60 @@ func NewBrowserProcess( return nil, err } + meta := newLocalBrowserProcessMeta(cmd.Process, dataDir) + p := BrowserProcess{ ctx: ctx, cancel: ctxCancel, - process: cmd.Process, + meta: meta, lostConnection: make(chan struct{}), processIsGracefullyClosing: make(chan struct{}), processDone: cmd.done, wsURL: wsURL, - userDataDir: dataDir, + logger: logger, } - go func() { - // If we lose connection to the browser and we're not in-progress with clean - // browser-initiated termination then cancel the context to clean up. - select { - case <-p.lostConnection: - case <-ctx.Done(): - } + go p.handleClose(ctx) - select { - case <-p.processIsGracefullyClosing: - default: - p.cancel() - } - }() + return &p, nil +} + +// NewRemoteBrowserProcess returns a new BrowserProcess instance +// which references a remote browser process. +func NewRemoteBrowserProcess( + ctx context.Context, wsURL string, ctxCancel context.CancelFunc, logger *log.Logger, +) (*BrowserProcess, error) { + p := BrowserProcess{ + ctx: ctx, + cancel: ctxCancel, + meta: newRemoteBrowserProcessMeta(), + lostConnection: make(chan struct{}), + processIsGracefullyClosing: make(chan struct{}), + processDone: make(chan struct{}), + wsURL: wsURL, + logger: logger, + } + + go p.handleClose(ctx) return &p, nil } +func (p *BrowserProcess) handleClose(ctx context.Context) { + // If we lose connection to the browser and we're not in-progress with clean + // browser-initiated termination then cancel the context to clean up. + select { + case <-p.lostConnection: + case <-ctx.Done(): + } + + select { + case <-p.processIsGracefullyClosing: + default: + p.cancel() + } +} + func (p *BrowserProcess) didLoseConnection() { close(p.lostConnection) } @@ -110,14 +133,15 @@ func (p *BrowserProcess) WsURL() string { return p.wsURL } -// Pid returns the browser process ID. +// Pid returns the browser process ID, or -1 if this is unknown. func (p *BrowserProcess) Pid() int { - return p.process.Pid + return p.meta.Pid() } -// AttachLogger attaches a logger to the browser process. -func (p *BrowserProcess) AttachLogger(logger *log.Logger) { - p.logger = logger +// Cleanup cleans up the metadata associated with the browser +// process, mainly the browser data directory. +func (p *BrowserProcess) Cleanup() error { + return p.meta.Cleanup() //nolint:wrapcheck } type command struct { diff --git a/vendor/github.com/grafana/xk6-browser/common/browser_process_meta.go b/vendor/github.com/grafana/xk6-browser/common/browser_process_meta.go new file mode 100644 index 00000000000..9124a75021e --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/common/browser_process_meta.go @@ -0,0 +1,70 @@ +package common + +import ( + "os" + + "github.com/grafana/xk6-browser/storage" +) + +const ( + unknownProcessPid = -1 +) + +// browserProcessMeta handles the metadata associated with +// a browser process, especifically, the OS process handle +// and the associated browser data directory. +type browserProcessMeta interface { + Pid() int + Cleanup() error +} + +// localBrowserProcessMeta holds the metadata for local +// browser process. +type localBrowserProcessMeta struct { + process *os.Process + userDataDir *storage.Dir +} + +// newLocalBrowserProcessMeta returns a new BrowserProcessMeta +// for the given OS process and storage directory. +func newLocalBrowserProcessMeta( + process *os.Process, userDataDir *storage.Dir, +) *localBrowserProcessMeta { + return &localBrowserProcessMeta{ + process, + userDataDir, + } +} + +// Pid returns the Pid for the local browser process. +func (l *localBrowserProcessMeta) Pid() int { + return l.process.Pid +} + +// Cleanup cleans the local user data directory associated +// with the local browser process. +func (l *localBrowserProcessMeta) Cleanup() error { + return l.userDataDir.Cleanup() //nolint:wrapcheck +} + +// remoteBrowserProcessMeta is a placeholder for a +// remote browser process metadata. +type remoteBrowserProcessMeta struct{} + +// newRemoteBrowserProcessMeta returns a new BrowserProcessMeta +// which acts as a placeholder for a remote browser process data. +func newRemoteBrowserProcessMeta() *remoteBrowserProcessMeta { + return &remoteBrowserProcessMeta{} +} + +// Pid returns -1 as the remote browser process is unknown. +func (r *remoteBrowserProcessMeta) Pid() int { + return unknownProcessPid +} + +// Cleanup does nothing and returns nil, as there is no +// access to the remote browser's user data directory. +func (r *remoteBrowserProcessMeta) Cleanup() error { + // Nothing to do. + return nil +} 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 49bf5a501c5..05f4be0ca5b 100644 --- a/vendor/github.com/grafana/xk6-browser/common/element_handle.go +++ b/vendor/github.com/grafana/xk6-browser/common/element_handle.go @@ -55,7 +55,11 @@ func (h *ElementHandle) boundingBox() (*Rect, error) { y := math.Min(quad[1], math.Min(quad[3], math.Min(quad[5], quad[7]))) width := math.Max(quad[0], math.Max(quad[2], math.Max(quad[4], quad[6]))) - x height := math.Max(quad[1], math.Max(quad[3], math.Max(quad[5], quad[7]))) - y - position := h.frame.position() + + position, err := h.frame.position() + if err != nil { + return nil, err + } return &Rect{X: x + position.X, Y: y + position.Y, Width: width, Height: height}, nil } @@ -63,10 +67,11 @@ func (h *ElementHandle) boundingBox() (*Rect, error) { func (h *ElementHandle) checkHitTargetAt(apiCtx context.Context, point Position) (bool, error) { frame := h.ownerFrame(apiCtx) if frame != nil && frame.parentFrame != nil { - var ( - el = h.frame.FrameElement() - element, ok = el.(*ElementHandle) - ) + el, err := h.frame.FrameElement() + if err != nil { + return false, err + } + element, ok := el.(*ElementHandle) if !ok { return false, fmt.Errorf("unexpected type %T", el) } @@ -755,20 +760,21 @@ func (h *ElementHandle) Click(opts goja.Value) error { return nil } -func (h *ElementHandle) ContentFrame() api.Frame { +// ContentFrame returns the frame that contains this element. +func (h *ElementHandle) ContentFrame() (api.Frame, error) { var ( node *cdp.Node err error ) action := dom.DescribeNode().WithObjectID(h.remoteObject.ObjectID) if node, err = action.Do(cdp.WithExecutor(h.ctx, h.session)); err != nil { - k6ext.Panic(h.ctx, "getting remote node %q: %w", h.remoteObject.ObjectID, err) + return nil, fmt.Errorf("getting remote node %q: %w", h.remoteObject.ObjectID, err) } if node == nil || node.FrameID == "" { - return nil + return nil, fmt.Errorf("element is not an iframe") } - return h.frame.manager.getFrameByID(node.FrameID) + return h.frame.manager.getFrameByID(node.FrameID), nil } func (h *ElementHandle) Dblclick(opts goja.Value) { @@ -969,7 +975,7 @@ func (h *ElementHandle) IsVisible() bool { } // OwnerFrame returns the frame containing this element. -func (h *ElementHandle) OwnerFrame() api.Frame { +func (h *ElementHandle) OwnerFrame() (api.Frame, error) { fn := ` (node, injected) => { return injected.getDocumentElement(node); @@ -981,28 +987,31 @@ func (h *ElementHandle) OwnerFrame() api.Frame { } res, err := h.evalWithScript(h.ctx, opts, fn) if err != nil { - k6ext.Panic(h.ctx, "getting document element: %w", err) + return nil, fmt.Errorf("getting document element: %w", err) } if res == nil { - return nil + return nil, errors.New("getting document element: nil document") } - documentHandle := res.(*ElementHandle) + documentHandle, ok := res.(*ElementHandle) + if !ok { + return nil, fmt.Errorf("unexpected result type while getting document element: %T", res) + } defer documentHandle.Dispose() if documentHandle.remoteObject.ObjectID == "" { - return nil + return nil, err } var node *cdp.Node action := dom.DescribeNode().WithObjectID(documentHandle.remoteObject.ObjectID) if node, err = action.Do(cdp.WithExecutor(h.ctx, h.session)); err != nil { - k6ext.Panic(h.ctx, "getting node in frame: %w", err) + return nil, fmt.Errorf("getting node in frame: %w", err) } if node == nil || node.FrameID == "" { - return nil + return nil, fmt.Errorf("no frame found for node: %w", err) } - return h.frame.manager.getFrameByID(node.FrameID) + return h.frame.manager.getFrameByID(node.FrameID), nil } func (h *ElementHandle) Press(key string, opts goja.Value) { @@ -1023,7 +1032,7 @@ func (h *ElementHandle) Press(key string, opts goja.Value) { // 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) api.ElementHandle { +func (h *ElementHandle) Query(selector string) (api.ElementHandle, error) { parsedSelector, err := NewSelector(selector) if err != nil { k6ext.Panic(h.ctx, "parsing selector %q: %w", selector, err) @@ -1039,35 +1048,35 @@ func (h *ElementHandle) Query(selector string) api.ElementHandle { } result, err := h.evalWithScript(h.ctx, opts, fn, parsedSelector) if err != nil { - k6ext.Panic(h.ctx, "querying selector %q: %w", selector, err) + return nil, fmt.Errorf("querying selector %q: %w", selector, err) } if result == nil { - return nil + return nil, fmt.Errorf("querying selector %q", selector) } - - var ( - handle = result.(api.JSHandle) - element = handle.AsElement() - ) - applySlowMo(h.ctx) - if element != nil { - return element + handle, ok := result.(api.JSHandle) + if !ok { + return nil, fmt.Errorf("querying selector %q, wrong type %T", selector, result) } - handle.Dispose() - return nil + element := handle.AsElement() + if element == nil { + handle.Dispose() + return nil, fmt.Errorf("querying selector %q", selector) + } + + return element, nil } // 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) []api.ElementHandle { +func (h *ElementHandle) QueryAll(selector string) ([]api.ElementHandle, error) { defer applySlowMo(h.ctx) handles, err := h.queryAll(selector, h.evalWithScript) if err != nil { - k6ext.Panic(h.ctx, "querying all selector %q: %w", selector, err) + return nil, fmt.Errorf("querying all selector %q: %w", selector, err) } - return handles + return handles, nil } func (h *ElementHandle) queryAll(selector string, eval evalFunc) ([]api.ElementHandle, error) { @@ -1094,10 +1103,14 @@ func (h *ElementHandle) queryAll(selector string, eval evalFunc) ([]api.ElementH return nil, fmt.Errorf("getting element handle for selector %q: %w", selector, ErrJSHandleInvalid) } defer handles.Dispose() - var ( - props = handles.GetProperties() - els = make([]api.ElementHandle, 0, len(props)) - ) + + props, err := handles.GetProperties() + if err != nil { + // GetProperties has a rich error already, so we don't need to wrap it. + return nil, err //nolint:wrapcheck + } + + els := make([]api.ElementHandle, 0, len(props)) for _, prop := range props { if el := prop.AsElement(); el != nil { els = append(els, el) @@ -1300,18 +1313,19 @@ func (h *ElementHandle) WaitForElementState(state string, opts goja.Value) { } } -func (h *ElementHandle) WaitForSelector(selector string, opts goja.Value) api.ElementHandle { +// WaitForSelector waits for the selector to appear in the DOM. +func (h *ElementHandle) WaitForSelector(selector string, opts goja.Value) (api.ElementHandle, error) { parsedOpts := NewFrameWaitForSelectorOptions(h.defaultTimeout()) if err := parsedOpts.Parse(h.ctx, opts); err != nil { - k6ext.Panic(h.ctx, "parsing waitForSelector %q options: %w", selector, err) + return nil, fmt.Errorf("parsing waitForSelector %q options: %w", selector, err) } handle, err := h.waitForSelector(h.ctx, selector, parsedOpts) if err != nil { - k6ext.Panic(h.ctx, "waiting for selector %q: %w", selector, err) + return nil, fmt.Errorf("waiting for selector %q: %w", selector, err) } - return handle + return handle, nil } // evalWithScript evaluates the given js code in the scope of this ElementHandle and returns the result. 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 d33fabf4ffc..2351c967747 100644 --- a/vendor/github.com/grafana/xk6-browser/common/execution_context.go +++ b/vendor/github.com/grafana/xk6-browser/common/execution_context.go @@ -321,6 +321,10 @@ func (e *ExecutionContext) EvalHandle( if err != nil { return nil, err } + if res == nil { + return nil, errors.New("nil result") + } + return res.(api.JSHandle), nil } diff --git a/vendor/github.com/grafana/xk6-browser/common/frame.go b/vendor/github.com/grafana/xk6-browser/common/frame.go index 54f30aeec80..9a24e4432b5 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame.go @@ -358,17 +358,22 @@ func (f *Frame) onLoadingStopped() { // website never stops performing network requests. } -func (f *Frame) position() *Position { +func (f *Frame) position() (*Position, error) { frame := f.manager.getFrameByID(cdp.FrameID(f.page.targetID)) if frame == nil { - return nil + return nil, fmt.Errorf("could not find frame with id %s", f.page.targetID) } if frame == f.page.frameManager.MainFrame() { - return &Position{X: 0, Y: 0} + return &Position{X: 0, Y: 0}, nil + } + element, err := frame.FrameElement() + if err != nil { + return nil, err } - element := frame.FrameElement() + box := element.BoundingBox() - return &Position{X: box.X, Y: box.Y} + + return &Position{X: box.X, Y: box.Y}, nil } func (f *Frame) removeChildFrame(child *Frame) { @@ -742,7 +747,7 @@ func (f *Frame) Evaluate(pageFunc goja.Value, args ...goja.Value) any { } // EvaluateHandle will evaluate provided page function within an execution context. -func (f *Frame) EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (handle api.JSHandle) { +func (f *Frame) EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (handle api.JSHandle, _ error) { f.log.Debugf("Frame:EvaluateHandle", "fid:%s furl:%q", f.ID(), f.URL()) f.waitForExecutionContext(mainWorld) @@ -752,17 +757,18 @@ func (f *Frame) EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (handle { ec := f.executionContexts[mainWorld] if ec == nil { - k6ext.Panic(f.ctx, "evaluating handle: execution context %q not found", mainWorld) + k6ext.Panic(f.ctx, "evaluating handle for frame: execution context %q not found", mainWorld) } handle, err = ec.EvalHandle(f.ctx, pageFunc, args...) } f.executionContextMu.RUnlock() if err != nil { - k6ext.Panic(f.ctx, "evaluating handle: %w", err) + return nil, fmt.Errorf("evaluating handle for frame: %w", err) } applySlowMo(f.ctx) - return handle + + return handle, nil } // Fill fills out the first element found that matches the selector. @@ -824,14 +830,15 @@ func (f *Frame) focus(selector string, opts *FrameBaseOptions) error { return nil } -func (f *Frame) FrameElement() api.ElementHandle { +// FrameElement returns the element handle for the frame. +func (f *Frame) FrameElement() (api.ElementHandle, error) { f.log.Debugf("Frame:FrameElement", "fid:%s furl:%q", f.ID(), f.URL()) element, err := f.page.getFrameElement(f) if err != nil { - k6ext.Panic(f.ctx, "getting frame element: %w", err) + return nil, fmt.Errorf("getting frame element: %w", err) } - return element + return element, nil } // GetAttribute of the first element found that matches the selector. @@ -1304,32 +1311,25 @@ func (f *Frame) Name() string { // Query runs a selector query against the document tree, returning the first matching element or // "null" if no match is found. -func (f *Frame) Query(selector string) api.ElementHandle { +func (f *Frame) Query(selector string) (api.ElementHandle, error) { f.log.Debugf("Frame:Query", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) document, err := f.document() if err != nil { k6ext.Panic(f.ctx, "getting document: %w", err) } - value := document.Query(selector) - if value != nil { - return value - } - return nil + return document.Query(selector) } -func (f *Frame) QueryAll(selector string) []api.ElementHandle { +// QueryAll runs a selector query against the document tree, returning all matching elements. +func (f *Frame) QueryAll(selector string) ([]api.ElementHandle, error) { f.log.Debugf("Frame:QueryAll", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) document, err := f.document() if err != nil { k6ext.Panic(f.ctx, "getting document: %w", err) } - value := document.QueryAll(selector) - if value != nil { - return value - } - return nil + return document.QueryAll(selector) } // Page returns page that owns frame. @@ -1826,16 +1826,17 @@ func (f *Frame) WaitForNavigation(opts goja.Value) (api.Response, error) { } // WaitForSelector waits for the given selector to match the waiting criteria. -func (f *Frame) WaitForSelector(selector string, opts goja.Value) api.ElementHandle { +func (f *Frame) WaitForSelector(selector string, opts goja.Value) (api.ElementHandle, error) { parsedOpts := NewFrameWaitForSelectorOptions(f.defaultTimeout()) if err := parsedOpts.Parse(f.ctx, opts); err != nil { - k6ext.Panic(f.ctx, "parsing wait for selector %q options: %w", selector, err) + return nil, fmt.Errorf("parsing wait for selector %q options: %w", selector, err) } handle, err := f.waitForSelectorRetry(selector, parsedOpts, maxRetry) if err != nil { - k6ext.Panic(f.ctx, "waiting for selector %q: %w", selector, err) + return nil, fmt.Errorf("waiting for selector %q: %w", selector, err) } - return handle + + return handle, nil } // WaitForTimeout waits the specified amount of milliseconds. 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 91da1b4b19c..2bd3bc2fa92 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame_session.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame_session.go @@ -8,6 +8,7 @@ import ( "runtime" "strings" "sync" + "time" "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/k6ext" @@ -261,12 +262,83 @@ func (fs *FrameSession) initEvents() { fs.onDetachedFromTarget(ev) case *cdppage.EventJavascriptDialogOpening: fs.onEventJavascriptDialogOpening(ev) + case *cdpruntime.EventBindingCalled: + fs.onEventBindingCalled(ev) } } } }() } +func (fs *FrameSession) onEventBindingCalled(event *cdpruntime.EventBindingCalled) { + fs.logger.Debugf("FrameSessions:onEventBindingCalled", + "sid:%v tid:%v name:%s payload:%s", + fs.session.ID(), fs.targetID, event.Name, event.Payload) + + err := fs.parseAndEmitWebVitalMetric(event.Payload) + if err != nil { + fs.logger.Errorf("FrameSession:onEventBindingCalled", "failed to emit web vital metric: %v", err) + } +} + +func (fs *FrameSession) parseAndEmitWebVitalMetric(object string) error { + fs.logger.Debugf("FrameSession:parseAndEmitWebVitalMetric", "object:%s", object) + + wv := struct { + ID string + Name string + Value json.Number + Rating string + Delta json.Number + NumEntries json.Number + NavigationType string + URL string + }{} + + if err := json.Unmarshal([]byte(object), &wv); err != nil { + return fmt.Errorf("json couldn't be parsed: %w", err) + } + + metric, ok := fs.k6Metrics.WebVitals[wv.Name] + if !ok { + return fmt.Errorf("metric not registered %q", wv.Name) + } + + metricRating, ok := fs.k6Metrics.WebVitals[k6ext.ConcatWebVitalNameRating(wv.Name, wv.Rating)] + if !ok { + return fmt.Errorf("metric not registered %q", k6ext.ConcatWebVitalNameRating(wv.Name, wv.Rating)) + } + + value, err := wv.Value.Float64() + if err != nil { + return fmt.Errorf("value couldn't be parsed %q", wv.Value) + } + + state := fs.vu.State() + tags := state.Tags.GetCurrentValues().Tags + if state.Options.SystemTags.Has(k6metrics.TagURL) { + tags = tags.With("url", wv.URL) + } + + now := time.Now() + k6metrics.PushIfNotDone(fs.ctx, state.Samples, k6metrics.ConnectedSamples{ + Samples: []k6metrics.Sample{ + { + TimeSeries: k6metrics.TimeSeries{Metric: metric, Tags: tags}, + Value: value, + Time: now, + }, + { + TimeSeries: k6metrics.TimeSeries{Metric: metricRating, Tags: tags}, + Value: 1, + Time: now, + }, + }, + }) + + return nil +} + func (fs *FrameSession) onEventJavascriptDialogOpening(event *cdppage.EventJavascriptDialogOpening) { fs.logger.Debugf("FrameSession:onEventJavascriptDialogOpening", "sid:%v tid:%v url:%v dialogType:%s", @@ -471,6 +543,7 @@ func (fs *FrameSession) initRendererEvents() { cdproto.EventRuntimeExecutionContextsCleared, cdproto.EventTargetAttachedToTarget, cdproto.EventTargetDetachedFromTarget, + cdproto.EventRuntimeBindingCalled, } fs.session.on(fs.ctx, events, fs.eventCh) } @@ -736,7 +809,6 @@ func (fs *FrameSession) onPageLifecycle(event *cdppage.EventLifecycleEvent) { "DOMContentLoaded": fs.k6Metrics.BrowserDOMContentLoaded, "firstPaint": fs.k6Metrics.BrowserFirstPaint, "firstContentfulPaint": fs.k6Metrics.BrowserFirstContentfulPaint, - "firstMeaningfulPaint": fs.k6Metrics.BrowserFirstMeaningfulPaint, } if m, ok := eventToMetric[event.Name]; ok { diff --git a/vendor/github.com/grafana/xk6-browser/common/js/web_vital.go b/vendor/github.com/grafana/xk6-browser/common/js/web_vital.go new file mode 100644 index 00000000000..e31da9ac76c --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/common/js/web_vital.go @@ -0,0 +1,19 @@ +package js + +import ( + _ "embed" +) + +// WebVitalIIFEScript was downloaded from +// https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js. +// Repo: https://github.com/GoogleChrome/web-vitals +// +//go:embed web_vital_iife.js +var WebVitalIIFEScript string + +// WebVitalInitScript uses WebVitalIIFEScript +// and applies it to the current website that +// this init script is used against. +// +//go:embed web_vital_init.js +var WebVitalInitScript string diff --git a/vendor/github.com/grafana/xk6-browser/common/js/web_vital_iife.js b/vendor/github.com/grafana/xk6-browser/common/js/web_vital_iife.js new file mode 100644 index 00000000000..2eaf56b92d9 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/common/js/web_vital_iife.js @@ -0,0 +1 @@ +var webVitals=function(e){"use strict";var n,t,r,i,o,a=-1,c=function(e){addEventListener("pageshow",(function(n){n.persisted&&(a=n.timeStamp,e(n))}),!0)},u=function(){return window.performance&&performance.getEntriesByType&&performance.getEntriesByType("navigation")[0]},s=function(){var e=u();return e&&e.activationStart||0},f=function(e,n){var t=u(),r="navigate";return a>=0?r="back-forward-cache":t&&(r=document.prerendering||s()>0?"prerender":document.wasDiscarded?"restore":t.type.replace(/_/g,"-")),{name:e,value:void 0===n?-1:n,rating:"good",delta:0,entries:[],id:"v3-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12),navigationType:r}},d=function(e,n,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){var r=new PerformanceObserver((function(e){Promise.resolve().then((function(){n(e.getEntries())}))}));return r.observe(Object.assign({type:e,buffered:!0},t||{})),r}}catch(e){}},l=function(e,n,t,r){var i,o;return function(a){n.value>=0&&(a||r)&&((o=n.value-(i||0))||void 0===i)&&(i=n.value,n.delta=o,n.rating=function(e,n){return e>n[1]?"poor":e>n[0]?"needs-improvement":"good"}(n.value,t),e(n))}},v=function(e){requestAnimationFrame((function(){return requestAnimationFrame((function(){return e()}))}))},p=function(e){var n=function(n){"pagehide"!==n.type&&"hidden"!==document.visibilityState||e(n)};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},m=function(e){var n=!1;return function(t){n||(e(t),n=!0)}},h=-1,g=function(){return"hidden"!==document.visibilityState||document.prerendering?1/0:0},T=function(e){"hidden"===document.visibilityState&&h>-1&&(h="visibilitychange"===e.type?e.timeStamp:0,C())},y=function(){addEventListener("visibilitychange",T,!0),addEventListener("prerenderingchange",T,!0)},C=function(){removeEventListener("visibilitychange",T,!0),removeEventListener("prerenderingchange",T,!0)},E=function(){return h<0&&(h=g(),y(),c((function(){setTimeout((function(){h=g(),y()}),0)}))),{get firstHiddenTime(){return h}}},L=function(e){document.prerendering?addEventListener("prerenderingchange",(function(){return e()}),!0):e()},b=[1800,3e3],S=function(e,n){n=n||{},L((function(){var t,r=E(),i=f("FCP"),o=d("paint",(function(e){e.forEach((function(e){"first-contentful-paint"===e.name&&(o.disconnect(),e.startTimer.value&&(r.value=i,r.entries=o,t())},u=d("layout-shift",a);u&&(t=l(e,r,w,n.reportAllChanges),p((function(){a(u.takeRecords()),t(!0)})),c((function(){i=0,r=f("CLS",0),t=l(e,r,w,n.reportAllChanges),v((function(){return t()}))})),setTimeout(t,0))})))},F={passive:!0,capture:!0},I=new Date,A=function(e,i){n||(n=i,t=e,r=new Date,k(removeEventListener),M())},M=function(){if(t>=0&&t1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,n){var t=function(){A(e,n),i()},r=function(){i()},i=function(){removeEventListener("pointerup",t,F),removeEventListener("pointercancel",r,F)};addEventListener("pointerup",t,F),addEventListener("pointercancel",r,F)}(n,e):A(n,e)}},k=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(n){return e(n,D,F)}))},B=[100,300],x=function(e,r){r=r||{},L((function(){var o,a=E(),u=f("FID"),s=function(e){e.startTimen.latency){if(t)t.entries.push(e),t.latency=Math.max(t.latency,e.duration);else{var r={id:e.interactionId,latency:e.duration,entries:[e]};J[r.id]=r,G.push(r)}G.sort((function(e,n){return n.latency-e.latency})),G.splice(10).forEach((function(e){delete J[e.id]}))}},Q=function(e,n){n=n||{},L((function(){j();var t,r=f("INP"),i=function(e){e.forEach((function(e){(e.interactionId&&K(e),"first-input"===e.entryType)&&(!G.some((function(n){return n.entries.some((function(n){return e.duration===n.duration&&e.startTime===n.startTime}))}))&&K(e))}));var n,i=(n=Math.min(G.length-1,Math.floor(z()/50)),G[n]);i&&i.latency!==r.value&&(r.value=i.latency,r.entries=i.entries,t())},o=d("event",i,{durationThreshold:n.durationThreshold||40});t=l(e,r,q,n.reportAllChanges),o&&(o.observe({type:"first-input",buffered:!0}),p((function(){i(o.takeRecords()),r.value<0&&z()>0&&(r.value=0,r.entries=[]),t(!0)})),c((function(){G=[],V=_(),r=f("INP"),t=l(e,r,q,n.reportAllChanges)})))}))},U=[2500,4e3],W={},X=function(e,n){n=n||{},L((function(){var t,r=E(),i=f("LCP"),o=function(e){var n=e[e.length-1];n&&n.startTimeperformance.now())return;t.value=Math.max(o-s(),0),t.entries=[i],r(!0),c((function(){t=f("TTFB",0),(r=l(e,t,Y,n.reportAllChanges))(!0)}))}}))};return e.CLSThresholds=w,e.FCPThresholds=b,e.FIDThresholds=B,e.INPThresholds=q,e.LCPThresholds=U,e.TTFBThresholds=Y,e.getCLS=P,e.getFCP=S,e.getFID=x,e.getINP=Q,e.getLCP=X,e.getTTFB=$,e.onCLS=P,e.onFCP=S,e.onFID=x,e.onINP=Q,e.onLCP=X,e.onTTFB=$,Object.defineProperty(e,"__esModule",{value:!0}),e}({}); \ No newline at end of file diff --git a/vendor/github.com/grafana/xk6-browser/common/js/web_vital_init.js b/vendor/github.com/grafana/xk6-browser/common/js/web_vital_init.js new file mode 100644 index 00000000000..d59d7f5539c --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/common/js/web_vital_init.js @@ -0,0 +1,25 @@ +function print(metric) { + const m = { + id: metric.id, + name: metric.name, + value: metric.value, + rating: metric.rating, + delta: metric.delta, + numEntries: metric.entries.length, + navigationType: metric.navigationType, + url: window.location.href, + } + window.k6browserSendWebVitalMetric(JSON.stringify(m)) +} + +function load() { + webVitals.onCLS(print); + webVitals.onFID(print); + webVitals.onLCP(print); + + webVitals.onFCP(print); + webVitals.onINP(print); + webVitals.onTTFB(print); +} + +load(); 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 a8c7d8c42ac..52ab253dff5 100644 --- a/vendor/github.com/grafana/xk6-browser/common/js_handle.go +++ b/vendor/github.com/grafana/xk6-browser/common/js_handle.go @@ -101,21 +101,23 @@ func (h *BaseJSHandle) Evaluate(pageFunc goja.Value, args ...goja.Value) any { } // EvaluateHandle will evaluate provided page function within an execution context. -func (h *BaseJSHandle) EvaluateHandle(pageFunc goja.Value, args ...goja.Value) api.JSHandle { +func (h *BaseJSHandle) EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (api.JSHandle, error) { rt := h.execCtx.vu.Runtime() args = append([]goja.Value{rt.ToValue(h)}, args...) - res, err := h.execCtx.EvalHandle(h.ctx, pageFunc, args...) + + eh, err := h.execCtx.EvalHandle(h.ctx, pageFunc, args...) if err != nil { - k6ext.Panic(h.ctx, "%w", err) + return nil, fmt.Errorf("evaluating handle for element: %w", err) } - return res + + return eh, nil } // GetProperties retreives the JS handle's properties. -func (h *BaseJSHandle) GetProperties() map[string]api.JSHandle { +func (h *BaseJSHandle) GetProperties() (map[string]api.JSHandle, error) { handles, err := h.getProperties() if err != nil { - k6ext.Panic(h.ctx, "getProperties: %w", err) + return nil, err } jsHandles := make(map[string]api.JSHandle, len(handles)) @@ -123,7 +125,7 @@ func (h *BaseJSHandle) GetProperties() map[string]api.JSHandle { jsHandles[k] = v } - return jsHandles + return jsHandles, nil } // getProperties is like GetProperties, but does not panic. diff --git a/vendor/github.com/grafana/xk6-browser/common/keyboard.go b/vendor/github.com/grafana/xk6-browser/common/keyboard.go index d56c4a6890e..cd2665d6094 100644 --- a/vendor/github.com/grafana/xk6-browser/common/keyboard.go +++ b/vendor/github.com/grafana/xk6-browser/common/keyboard.go @@ -182,6 +182,7 @@ func (k *Keyboard) keyDefinitionFromKey(key keyboardlayout.KeyInput) keyboardlay } var keyDef keyboardlayout.KeyDefinition + keyDef.Code = srcKeyDef.Code if srcKeyDef.Key != "" { keyDef.Key = srcKeyDef.Key } @@ -194,9 +195,6 @@ func (k *Keyboard) keyDefinitionFromKey(key keyboardlayout.KeyInput) keyboardlay if srcKeyDef.KeyCode != 0 { keyDef.KeyCode = srcKeyDef.KeyCode } - if key != "" { - keyDef.Code = string(key) - } if srcKeyDef.Location != 0 { keyDef.Location = srcKeyDef.Location } diff --git a/vendor/github.com/grafana/xk6-browser/common/locator.go b/vendor/github.com/grafana/xk6-browser/common/locator.go index ce5608cf72d..9720e56244c 100644 --- a/vendor/github.com/grafana/xk6-browser/common/locator.go +++ b/vendor/github.com/grafana/xk6-browser/common/locator.go @@ -31,21 +31,20 @@ func NewLocator(ctx context.Context, selector string, f *Frame, l *log.Logger) * } // Click on an element using locator's selector with strict mode on. -func (l *Locator) Click(opts goja.Value) { +func (l *Locator) Click(opts goja.Value) error { l.log.Debugf("Locator:Click", "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 := NewFrameClickOptions(l.frame.defaultTimeout()) - if err = copts.Parse(l.ctx, opts); err != nil { - err = fmt.Errorf("parsing click options: %w", err) - return + if err := copts.Parse(l.ctx, opts); err != nil { + return fmt.Errorf("parsing click options: %w", err) } - if err = l.click(copts); err != nil { - err = fmt.Errorf("clicking on %q: %w", l.selector, err) - return + if err := l.click(copts); err != nil { + return fmt.Errorf("clicking on %q: %w", l.selector, err) } + + applySlowMo(l.ctx) + + return nil } // click is like Click but takes parsed options and neither throws an diff --git a/vendor/github.com/grafana/xk6-browser/common/page.go b/vendor/github.com/grafana/xk6-browser/common/page.go index 41100971e20..4f888c66877 100644 --- a/vendor/github.com/grafana/xk6-browser/common/page.go +++ b/vendor/github.com/grafana/xk6-browser/common/page.go @@ -17,11 +17,15 @@ import ( "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/dom" "github.com/chromedp/cdproto/emulation" + "github.com/chromedp/cdproto/page" cdppage "github.com/chromedp/cdproto/page" + "github.com/chromedp/cdproto/runtime" "github.com/chromedp/cdproto/target" "github.com/dop251/goja" ) +const webVitalBinding = "k6browserSendWebVitalMetric" + // Ensure page implements the EventEmitter, Target and Page interfaces. var ( _ EventEmitter = &Page{} @@ -134,6 +138,15 @@ func NewPage( return nil, fmt.Errorf("internal error while auto attaching to browser pages: %w", err) } + add := runtime.AddBinding(webVitalBinding) + if err := add.Do(cdp.WithExecutor(p.ctx, p.session)); err != nil { + return nil, fmt.Errorf("internal error while adding binding to page: %w", err) + } + + if err := bctx.applyAllInitScripts(&p); err != nil { + return nil, fmt.Errorf("internal error while applying init scripts to page: %w", err) + } + return &p, nil } @@ -168,8 +181,16 @@ func (p *Page) didCrash() { p.emit(EventPageCrash, p) } -func (p *Page) evaluateOnNewDocument(source string) { - // TODO: implement +func (p *Page) evaluateOnNewDocument(source string) error { + p.logger.Debugf("Page:evaluateOnNewDocument", "sid:%v", p.sessionID()) + + action := page.AddScriptToEvaluateOnNewDocument(source) + _, err := action.Do(cdp.WithExecutor(p.ctx, p.session)) + if err != nil { + return fmt.Errorf("evaluating script on document: %w", err) + } + + return nil } func (p *Page) getFrameElement(f *Frame) (handle *ElementHandle, _ error) { @@ -401,10 +422,34 @@ func (p *Page) Click(selector string, opts goja.Value) error { } // Close closes the page. -func (p *Page) Close(opts goja.Value) { +func (p *Page) Close(opts goja.Value) error { p.logger.Debugf("Page:Close", "sid:%v", p.sessionID()) - p.browserCtx.Close() + add := runtime.RemoveBinding(webVitalBinding) + if err := add.Do(cdp.WithExecutor(p.ctx, p.session)); err != nil { + return fmt.Errorf("internal error while removing binding from page: %w", err) + } + + action := target.CloseTarget(p.targetID) + err := action.Do(cdp.WithExecutor(p.ctx, p.session)) + if err != nil { + // When a close target command is sent to the browser via CDP, + // the browser will start to cleanup and the first thing it + // will do is return a target.EventDetachedFromTarget, which in + // our implementation will close the session connection (this + // does not close the CDP websocket, just removes the session + // so no other CDP calls can be made with the session ID). + // This can result in the session's context being closed while + // we're waiting for the response to come back from the browser + // for this current command (it's racey). + if errors.Is(err, context.Canceled) { + return nil + } + + return fmt.Errorf("closing a page: %w", err) + } + + return nil } // Content returns the HTML content of the page. @@ -490,10 +535,15 @@ func (p *Page) Evaluate(pageFunc goja.Value, args ...goja.Value) any { return p.MainFrame().Evaluate(pageFunc, args...) } -func (p *Page) EvaluateHandle(pageFunc goja.Value, args ...goja.Value) api.JSHandle { +// EvaluateHandle runs JS code within the execution context of the main frame of the page. +func (p *Page) EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (api.JSHandle, error) { p.logger.Debugf("Page:EvaluateHandle", "sid:%v", p.sessionID()) - return p.MainFrame().EvaluateHandle(pageFunc, args...) + h, err := p.MainFrame().EvaluateHandle(pageFunc, args...) + if err != nil { + return nil, fmt.Errorf("evaluating handle for page: %w", err) + } + return h, nil } // ExposeBinding is not implemented. @@ -675,13 +725,15 @@ func (p *Page) Press(selector string, key string, opts goja.Value) { p.MainFrame().Press(selector, key, opts) } -func (p *Page) Query(selector string) api.ElementHandle { +// Query returns the first element matching the specified selector. +func (p *Page) Query(selector string) (api.ElementHandle, error) { p.logger.Debugf("Page:Query", "sid:%v selector:%s", p.sessionID(), selector) return p.frameManager.MainFrame().Query(selector) } -func (p *Page) QueryAll(selector string) []api.ElementHandle { +// QueryAll returns all elements matching the specified selector. +func (p *Page) QueryAll(selector string) ([]api.ElementHandle, error) { p.logger.Debugf("Page:QueryAll", "sid:%v selector:%s", p.sessionID(), selector) return p.frameManager.MainFrame().QueryAll(selector) @@ -932,7 +984,7 @@ func (p *Page) WaitForResponse(urlOrPredicate, opts goja.Value) api.Response { } // WaitForSelector waits for the given selector to match the waiting criteria. -func (p *Page) WaitForSelector(selector string, opts goja.Value) api.ElementHandle { +func (p *Page) WaitForSelector(selector string, opts goja.Value) (api.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/session.go b/vendor/github.com/grafana/xk6-browser/common/session.go index 30c4734730f..e5c531482f7 100644 --- a/vendor/github.com/grafana/xk6-browser/common/session.go +++ b/vendor/github.com/grafana/xk6-browser/common/session.go @@ -113,10 +113,6 @@ func (s *Session) readLoop() { // Execute implements the cdp.Executor interface. func (s *Session) Execute(ctx context.Context, method string, params easyjson.Marshaler, res easyjson.Unmarshaler) error { s.logger.Debugf("Session:Execute", "sid:%v tid:%v method:%q", s.id, s.targetID, method) - // Certain methods aren't available to the user directly. - if method == target.CommandCloseTarget { - return errors.New("to close the target, cancel its context") - } if s.crashed { s.logger.Debugf("Session:Execute:return", "sid:%v tid:%v method:%q crashed", s.id, s.targetID, method) return ErrTargetCrashed @@ -173,10 +169,6 @@ func (s *Session) Execute(ctx context.Context, method string, params easyjson.Ma func (s *Session) ExecuteWithoutExpectationOnReply(ctx context.Context, method string, params easyjson.Marshaler, res easyjson.Unmarshaler) error { s.logger.Debugf("Session:ExecuteWithoutExpectationOnReply", "sid:%v tid:%v method:%q", s.id, s.targetID, method) - // Certain methods aren't available to the user directly. - if method == target.CommandCloseTarget { - return errors.New("to close the target, cancel its context") - } if s.crashed { s.logger.Debugf("Session:ExecuteWithoutExpectationOnReply", "sid:%v tid:%v method:%q, ErrTargetCrashed", s.id, s.targetID, method) return ErrTargetCrashed diff --git a/vendor/github.com/grafana/xk6-browser/common/worker.go b/vendor/github.com/grafana/xk6-browser/common/worker.go index 8da76bc72d4..12d0a186bf2 100644 --- a/vendor/github.com/grafana/xk6-browser/common/worker.go +++ b/vendor/github.com/grafana/xk6-browser/common/worker.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/grafana/xk6-browser/api" + "github.com/grafana/xk6-browser/k6error" "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/log" @@ -69,9 +70,9 @@ func (w *Worker) Evaluate(pageFunc goja.Value, args ...goja.Value) any { } // EvaluateHandle evaluates a page function in the context of the web worker and returns a JS handle. -func (w *Worker) EvaluateHandle(pageFunc goja.Value, args ...goja.Value) api.JSHandle { +func (w *Worker) EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (api.JSHandle, error) { // TODO: implement - return nil + return nil, fmt.Errorf("Worker.EvaluateHandle has not been implemented yet: %w", k6error.ErrFatal) } // URL returns the URL of the web worker. diff --git a/vendor/github.com/grafana/xk6-browser/k6error/internal.go b/vendor/github.com/grafana/xk6-browser/k6error/internal.go new file mode 100644 index 00000000000..f59ba5656e9 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/k6error/internal.go @@ -0,0 +1,15 @@ +// Package k6error contains ErrFatal. +package k6error + +import ( + "errors" +) + +// ErrFatal should be wrapped into an error +// to signal to the mapping layer that the error +// is a fatal error and we should abort the whole +// test run, not just the current iteration. It +// should be used in cases where if the iteration +// ran again then there's a 100% chance that it +// will end up running into the same error. +var ErrFatal = errors.New("fatal error") diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/context.go b/vendor/github.com/grafana/xk6-browser/k6ext/context.go index 8138898c6e5..9c1de769823 100644 --- a/vendor/github.com/grafana/xk6-browser/k6ext/context.go +++ b/vendor/github.com/grafana/xk6-browser/k6ext/context.go @@ -33,17 +33,6 @@ func GetVU(ctx context.Context) k6modules.VU { return nil } -// WithProcessID saves the browser process ID to the context. -func WithProcessID(ctx context.Context, pid int) context.Context { - return context.WithValue(ctx, ctxKeyPid, pid) -} - -// GetProcessID returns the browser process ID from the context. -func GetProcessID(ctx context.Context) int { - v, _ := ctx.Value(ctxKeyPid).(int) - return v // it will return zero on error -} - // WithCustomMetrics attaches the CustomK6Metrics object to the context. func WithCustomMetrics(ctx context.Context, k6m *CustomMetrics) context.Context { return context.WithValue(ctx, ctxKeyCustomK6Metrics, k6m) diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go b/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go index 28240d09426..d4e86ba4150 100644 --- a/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go +++ b/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go @@ -1,19 +1,61 @@ package k6ext -import k6metrics "go.k6.io/k6/metrics" +import ( + "fmt" + + k6metrics "go.k6.io/k6/metrics" +) + +const ( + webVitalFID = "FID" + webVitalTTFB = "TTFB" + webVitalLCP = "LCP" + webVitalCLS = "CLS" + webVitalINP = "INP" + webVitalFCP = "FCP" +) // CustomMetrics are the custom k6 metrics used by xk6-browser. type CustomMetrics struct { BrowserDOMContentLoaded *k6metrics.Metric BrowserFirstPaint *k6metrics.Metric BrowserFirstContentfulPaint *k6metrics.Metric - BrowserFirstMeaningfulPaint *k6metrics.Metric BrowserLoaded *k6metrics.Metric + + WebVitals map[string]*k6metrics.Metric } // RegisterCustomMetrics creates and registers our custom metrics with the k6 // VU Registry and returns our internal struct pointer. func RegisterCustomMetrics(registry *k6metrics.Registry) *CustomMetrics { + wvs := map[string]string{ + webVitalFID: "webvital_first_input_delay", + webVitalTTFB: "webvital_time_to_first_byte", + webVitalLCP: "webvital_largest_content_paint", + webVitalCLS: "webvital_cumulative_layout_shift", + webVitalINP: "webvital_interaction_to_next_paint", + webVitalFCP: "webvital_first_contentful_paint", + } + webVitals := make(map[string]*k6metrics.Metric) + + for k, v := range wvs { + t := k6metrics.Time + // CLS is not a time based measurement, it is a score, + // so use the default metric type for CLS. + if k == webVitalCLS { + t = k6metrics.Default + } + + webVitals[k] = registry.MustNewMetric(v, k6metrics.Trend, t) + + webVitals[ConcatWebVitalNameRating(k, "good")] = registry.MustNewMetric( + v+"_good", k6metrics.Counter) + webVitals[ConcatWebVitalNameRating(k, "needs-improvement")] = registry.MustNewMetric( + v+"_needs_improvement", k6metrics.Counter) + webVitals[ConcatWebVitalNameRating(k, "poor")] = registry.MustNewMetric( + v+"_poor", k6metrics.Counter) + } + return &CustomMetrics{ BrowserDOMContentLoaded: registry.MustNewMetric( "browser_dom_content_loaded", k6metrics.Trend, k6metrics.Time), @@ -21,9 +63,16 @@ func RegisterCustomMetrics(registry *k6metrics.Registry) *CustomMetrics { "browser_first_paint", k6metrics.Trend, k6metrics.Time), BrowserFirstContentfulPaint: registry.MustNewMetric( "browser_first_contentful_paint", k6metrics.Trend, k6metrics.Time), - BrowserFirstMeaningfulPaint: registry.MustNewMetric( - "browser_first_meaningful_paint", k6metrics.Trend, k6metrics.Time), BrowserLoaded: registry.MustNewMetric( "browser_loaded", k6metrics.Trend, k6metrics.Time), + WebVitals: webVitals, } } + +// ConcatWebVitalNameRating can be used +// to create the correct metric key name +// to retrieve the corresponding metric +// from the registry. +func ConcatWebVitalNameRating(name, rating string) string { + return fmt.Sprintf("%s:%s", name, rating) +} diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/panic.go b/vendor/github.com/grafana/xk6-browser/k6ext/panic.go index ca011f8bda5..53611c88fb6 100644 --- a/vendor/github.com/grafana/xk6-browser/k6ext/panic.go +++ b/vendor/github.com/grafana/xk6-browser/k6ext/panic.go @@ -8,14 +8,36 @@ import ( "strings" "time" + "github.com/dop251/goja" + + "go.k6.io/k6/errext" k6common "go.k6.io/k6/js/common" ) -// Panic will cause a panic with the given error which will shut -// the application down. Before panicking, it will find the +// Abort will shutdown the whole test run. This should +// only be used from the goja 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) { + reason := fmt.Errorf(format, a...).Error() + rt.Interrupt(&errext.InterruptError{Reason: reason}) + } + sharedPanic(ctx, failFunc, a...) +} + +// Panic will cause a panic with the given error which will stop +// the current iteration. Before panicking, it will find the // 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) { + 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) { rt := Runtime(ctx) if rt == nil { // this should never happen unless a programmer error @@ -31,23 +53,26 @@ func Panic(ctx context.Context, format string, a ...any) { a[len(a)-1] = &UserFriendlyError{Err: err} } } - defer k6common.Throw(rt, fmt.Errorf(format, a...)) + defer failFunc(rt, a...) - pid := GetProcessID(ctx) - if pid == 0 { - // this should never happen unless a programmer error - panic("no browser process ID in context") - } - p, err := os.FindProcess(pid) - if err != nil { - // optimistically return and don't kill the process + // TODO: Remove this after moving k6ext.Panic into the mapping layer. + pidder, ok := GetVU(ctx).(interface { + Pids() []int + }) + if !ok { + // we're running in a test, let's skip killing the process. return } - // no need to check the error for waiting the process to release - // its resources or whether we could kill it as we're already - // dying. - _ = p.Release() - _ = p.Kill() + for _, pid := range pidder.Pids() { + p, err := os.FindProcess(pid) + if err != nil { + // optimistically skip and don't kill the process + continue + } + // no need to check the error for whether we could kill it as + // we're already dying. + _ = p.Kill() + } } // UserFriendlyError maps an internal error to an error that users diff --git a/vendor/github.com/grafana/xk6-browser/keyboardlayout/us.go b/vendor/github.com/grafana/xk6-browser/keyboardlayout/us.go index 0f9e1dce40c..0fd7d2bde0a 100644 --- a/vendor/github.com/grafana/xk6-browser/keyboardlayout/us.go +++ b/vendor/github.com/grafana/xk6-browser/keyboardlayout/us.go @@ -260,127 +260,127 @@ func initUS() { } Keys := map[KeyInput]KeyDefinition{ // Functions row - "Escape": {KeyCode: 27, Key: "Escape"}, - "F1": {KeyCode: 112, Key: "F1"}, - "F2": {KeyCode: 113, Key: "F2"}, - "F3": {KeyCode: 114, Key: "F3"}, - "F4": {KeyCode: 115, Key: "F4"}, - "F5": {KeyCode: 116, Key: "F5"}, - "F6": {KeyCode: 117, Key: "F6"}, - "F7": {KeyCode: 118, Key: "F7"}, - "F8": {KeyCode: 119, Key: "F8"}, - "F9": {KeyCode: 120, Key: "F9"}, - "F10": {KeyCode: 121, Key: "F10"}, - "F11": {KeyCode: 122, Key: "F11"}, - "F12": {KeyCode: 123, Key: "F12"}, + "Escape": {Code: "Escape", KeyCode: 27, Key: "Escape"}, + "F1": {Code: "F1", KeyCode: 112, Key: "F1"}, + "F2": {Code: "F2", KeyCode: 113, Key: "F2"}, + "F3": {Code: "F3", KeyCode: 114, Key: "F3"}, + "F4": {Code: "F4", KeyCode: 115, Key: "F4"}, + "F5": {Code: "F5", KeyCode: 116, Key: "F5"}, + "F6": {Code: "F6", KeyCode: 117, Key: "F6"}, + "F7": {Code: "F7", KeyCode: 118, Key: "F7"}, + "F8": {Code: "F8", KeyCode: 119, Key: "F8"}, + "F9": {Code: "F9", KeyCode: 120, Key: "F9"}, + "F10": {Code: "F10", KeyCode: 121, Key: "F10"}, + "F11": {Code: "F11", KeyCode: 122, Key: "F11"}, + "F12": {Code: "F12", KeyCode: 123, Key: "F12"}, // Numbers row - "Backquote": {KeyCode: 192, ShiftKey: "~", Key: "`"}, - "Digit1": {KeyCode: 49, ShiftKey: "!", Key: "1"}, - "Digit2": {KeyCode: 50, ShiftKey: "@", Key: "2"}, - "Digit3": {KeyCode: 51, ShiftKey: "#", Key: "3"}, - "Digit4": {KeyCode: 52, ShiftKey: "$", Key: "4"}, - "Digit5": {KeyCode: 53, ShiftKey: "%", Key: "5"}, - "Digit6": {KeyCode: 54, ShiftKey: "^", Key: "6"}, - "Digit7": {KeyCode: 55, ShiftKey: "&", Key: "7"}, - "Digit8": {KeyCode: 56, ShiftKey: "*", Key: "8"}, - "Digit9": {KeyCode: 57, ShiftKey: "(", Key: "9"}, - "Digit0": {KeyCode: 48, ShiftKey: ")", Key: "0"}, - "Minus": {KeyCode: 189, ShiftKey: "_", Key: "-"}, - "Equal": {KeyCode: 187, ShiftKey: "+", Key: "="}, - "Backslash": {KeyCode: 220, ShiftKey: "|", Key: "\\"}, - "Backspace": {KeyCode: 8, Key: "Backspace"}, + "Backquote": {Code: "Backquote", KeyCode: 192, ShiftKey: "~", Key: "`"}, + "Digit1": {Code: "Digit1", KeyCode: 49, ShiftKey: "!", Key: "1"}, + "Digit2": {Code: "Digit2", KeyCode: 50, ShiftKey: "@", Key: "2"}, + "Digit3": {Code: "Digit3", KeyCode: 51, ShiftKey: "#", Key: "3"}, + "Digit4": {Code: "Digit4", KeyCode: 52, ShiftKey: "$", Key: "4"}, + "Digit5": {Code: "Digit5", KeyCode: 53, ShiftKey: "%", Key: "5"}, + "Digit6": {Code: "Digit6", KeyCode: 54, ShiftKey: "^", Key: "6"}, + "Digit7": {Code: "Digit7", KeyCode: 55, ShiftKey: "&", Key: "7"}, + "Digit8": {Code: "Digit8", KeyCode: 56, ShiftKey: "*", Key: "8"}, + "Digit9": {Code: "Digit9", KeyCode: 57, ShiftKey: "(", Key: "9"}, + "Digit0": {Code: "Digit0", KeyCode: 48, ShiftKey: ")", Key: "0"}, + "Minus": {Code: "Minus", KeyCode: 189, ShiftKey: "_", Key: "-"}, + "Equal": {Code: "Equal", KeyCode: 187, ShiftKey: "+", Key: "="}, + "Backslash": {Code: "Backslash", KeyCode: 220, ShiftKey: "|", Key: "\\"}, + "Backspace": {Code: "Backspace", KeyCode: 8, Key: "Backspace"}, // First row - "Tab": {KeyCode: 9, Key: "Tab"}, - "KeyQ": {KeyCode: 81, ShiftKey: "Q", Key: "q"}, - "KeyW": {KeyCode: 87, ShiftKey: "W", Key: "w"}, - "KeyE": {KeyCode: 69, ShiftKey: "E", Key: "e"}, - "KeyR": {KeyCode: 82, ShiftKey: "R", Key: "r"}, - "KeyT": {KeyCode: 84, ShiftKey: "T", Key: "t"}, - "KeyY": {KeyCode: 89, ShiftKey: "Y", Key: "y"}, - "KeyU": {KeyCode: 85, ShiftKey: "U", Key: "u"}, - "KeyI": {KeyCode: 73, ShiftKey: "I", Key: "i"}, - "KeyO": {KeyCode: 79, ShiftKey: "O", Key: "o"}, - "KeyP": {KeyCode: 80, ShiftKey: "P", Key: "p"}, - "BracketLeft": {KeyCode: 219, ShiftKey: "{", Key: "["}, - "BracketRight": {KeyCode: 221, ShiftKey: "}", Key: "]"}, + "Tab": {Code: "Tab", KeyCode: 9, Key: "Tab"}, + "KeyQ": {Code: "KeyQ", KeyCode: 81, ShiftKey: "Q", Key: "q"}, + "KeyW": {Code: "KeyW", KeyCode: 87, ShiftKey: "W", Key: "w"}, + "KeyE": {Code: "KeyE", KeyCode: 69, ShiftKey: "E", Key: "e"}, + "KeyR": {Code: "KeyR", KeyCode: 82, ShiftKey: "R", Key: "r"}, + "KeyT": {Code: "KeyT", KeyCode: 84, ShiftKey: "T", Key: "t"}, + "KeyY": {Code: "KeyY", KeyCode: 89, ShiftKey: "Y", Key: "y"}, + "KeyU": {Code: "KeyU", KeyCode: 85, ShiftKey: "U", Key: "u"}, + "KeyI": {Code: "KeyI", KeyCode: 73, ShiftKey: "I", Key: "i"}, + "KeyO": {Code: "KeyO", KeyCode: 79, ShiftKey: "O", Key: "o"}, + "KeyP": {Code: "KeyP", KeyCode: 80, ShiftKey: "P", Key: "p"}, + "BracketLeft": {Code: "BracketLeft", KeyCode: 219, ShiftKey: "{", Key: "["}, + "BracketRight": {Code: "BracketRight", KeyCode: 221, ShiftKey: "}", Key: "]"}, // Second row - "CapsLock": {KeyCode: 20, Key: "CapsLock"}, - "KeyA": {KeyCode: 65, ShiftKey: "A", Key: "a"}, - "KeyS": {KeyCode: 83, ShiftKey: "S", Key: "s"}, - "KeyD": {KeyCode: 68, ShiftKey: "D", Key: "d"}, - "KeyF": {KeyCode: 70, ShiftKey: "F", Key: "f"}, - "KeyG": {KeyCode: 71, ShiftKey: "G", Key: "g"}, - "KeyH": {KeyCode: 72, ShiftKey: "H", Key: "h"}, - "KeyJ": {KeyCode: 74, ShiftKey: "J", Key: "j"}, - "KeyK": {KeyCode: 75, ShiftKey: "K", Key: "k"}, - "KeyL": {KeyCode: 76, ShiftKey: "L", Key: "l"}, - "Semicolon": {KeyCode: 186, ShiftKey: ":", Key: ";"}, - "Quote": {KeyCode: 222, ShiftKey: "\"", Key: "'"}, - "Enter": {KeyCode: 13, Key: "Enter", Text: "\r"}, + "CapsLock": {Code: "CapsLock", KeyCode: 20, Key: "CapsLock"}, + "KeyA": {Code: "KeyA", KeyCode: 65, ShiftKey: "A", Key: "a"}, + "KeyS": {Code: "KeyS", KeyCode: 83, ShiftKey: "S", Key: "s"}, + "KeyD": {Code: "KeyD", KeyCode: 68, ShiftKey: "D", Key: "d"}, + "KeyF": {Code: "KeyF", KeyCode: 70, ShiftKey: "F", Key: "f"}, + "KeyG": {Code: "KeyG", KeyCode: 71, ShiftKey: "G", Key: "g"}, + "KeyH": {Code: "KeyH", KeyCode: 72, ShiftKey: "H", Key: "h"}, + "KeyJ": {Code: "KeyJ", KeyCode: 74, ShiftKey: "J", Key: "j"}, + "KeyK": {Code: "KeyK", KeyCode: 75, ShiftKey: "K", Key: "k"}, + "KeyL": {Code: "KeyL", KeyCode: 76, ShiftKey: "L", Key: "l"}, + "Semicolon": {Code: "Semicolon", KeyCode: 186, ShiftKey: ":", Key: ";"}, + "Quote": {Code: "Quote", KeyCode: 222, ShiftKey: "\"", Key: "'"}, + "Enter": {Code: "Enter", KeyCode: 13, Key: "Enter", Text: "\r"}, // Third row - "ShiftLeft": {KeyCode: 160, KeyCodeWithoutLocation: 16, Key: "Shift", Location: 1}, - "KeyZ": {KeyCode: 90, ShiftKey: "Z", Key: "z"}, - "KeyX": {KeyCode: 88, ShiftKey: "X", Key: "x"}, - "KeyC": {KeyCode: 67, ShiftKey: "C", Key: "c"}, - "KeyV": {KeyCode: 86, ShiftKey: "V", Key: "v"}, - "KeyB": {KeyCode: 66, ShiftKey: "B", Key: "b"}, - "KeyN": {KeyCode: 78, ShiftKey: "N", Key: "n"}, - "KeyM": {KeyCode: 77, ShiftKey: "M", Key: "m"}, - "Comma": {KeyCode: 188, ShiftKey: "<", Key: ","}, - "Period": {KeyCode: 190, ShiftKey: ">", Key: "."}, - "Slash": {KeyCode: 191, ShiftKey: "?", Key: "/"}, - "ShiftRight": {KeyCode: 161, KeyCodeWithoutLocation: 16, Key: "Shift", Location: 2}, + "ShiftLeft": {Code: "ShiftLeft", KeyCode: 160, KeyCodeWithoutLocation: 16, Key: "Shift", Location: 1}, + "KeyZ": {Code: "KeyZ", KeyCode: 90, ShiftKey: "Z", Key: "z"}, + "KeyX": {Code: "KeyX", KeyCode: 88, ShiftKey: "X", Key: "x"}, + "KeyC": {Code: "KeyC", KeyCode: 67, ShiftKey: "C", Key: "c"}, + "KeyV": {Code: "KeyV", KeyCode: 86, ShiftKey: "V", Key: "v"}, + "KeyB": {Code: "KeyB", KeyCode: 66, ShiftKey: "B", Key: "b"}, + "KeyN": {Code: "KeyN", KeyCode: 78, ShiftKey: "N", Key: "n"}, + "KeyM": {Code: "KeyM", KeyCode: 77, ShiftKey: "M", Key: "m"}, + "Comma": {Code: "Comma", KeyCode: 188, ShiftKey: "<", Key: ","}, + "Period": {Code: "Period", KeyCode: 190, ShiftKey: ">", Key: "."}, + "Slash": {Code: "Slash", KeyCode: 191, ShiftKey: "?", Key: "/"}, + "ShiftRight": {Code: "ShiftRight", KeyCode: 161, KeyCodeWithoutLocation: 16, Key: "Shift", Location: 2}, // Last row - "ControlLeft": {KeyCode: 162, KeyCodeWithoutLocation: 17, Key: "Control", Location: 1}, - "MetaLeft": {KeyCode: 91, Key: "Meta", Location: 1}, - "AltLeft": {KeyCode: 164, KeyCodeWithoutLocation: 18, Key: "Alt", Location: 1}, - "Space": {KeyCode: 32, Key: " "}, - "AltRight": {KeyCode: 165, KeyCodeWithoutLocation: 18, Key: "Alt", Location: 2}, - "AltGraph": {KeyCode: 225, Key: "AltGraph"}, - "MetaRight": {KeyCode: 92, Key: "Meta", Location: 2}, - "ConTextMenu": {KeyCode: 93, Key: "ConTextMenu"}, - "ControlRight": {KeyCode: 163, KeyCodeWithoutLocation: 17, Key: "Control", Location: 2}, + "ControlLeft": {Code: "ControlLeft", KeyCode: 162, KeyCodeWithoutLocation: 17, Key: "Control", Location: 1}, + "MetaLeft": {Code: "MetaLeft", KeyCode: 91, Key: "Meta", Location: 1}, + "AltLeft": {Code: "AltLeft", KeyCode: 164, KeyCodeWithoutLocation: 18, Key: "Alt", Location: 1}, + "Space": {Code: "Space", KeyCode: 32, Key: " "}, + "AltRight": {Code: "AltRight", KeyCode: 165, KeyCodeWithoutLocation: 18, Key: "Alt", Location: 2}, + "AltGraph": {Code: "AltGraph", KeyCode: 225, Key: "AltGraph"}, + "MetaRight": {Code: "MetaRight", KeyCode: 92, Key: "Meta", Location: 2}, + "ConTextMenu": {Code: "ConTextMenu", KeyCode: 93, Key: "ConTextMenu"}, + "ControlRight": {Code: "ControlRight", KeyCode: 163, KeyCodeWithoutLocation: 17, Key: "Control", Location: 2}, // Center block - "PrintScreen": {KeyCode: 44, Key: "PrintScreen"}, - "ScrollLock": {KeyCode: 145, Key: "ScrollLock"}, - "Pause": {KeyCode: 19, Key: "Pause"}, + "PrintScreen": {Code: "PrintScreen", KeyCode: 44, Key: "PrintScreen"}, + "ScrollLock": {Code: "ScrollLock", KeyCode: 145, Key: "ScrollLock"}, + "Pause": {Code: "Pause", KeyCode: 19, Key: "Pause"}, - "PageUp": {KeyCode: 33, Key: "PageUp"}, - "PageDown": {KeyCode: 34, Key: "PageDown"}, - "Insert": {KeyCode: 45, Key: "Insert"}, - "Delete": {KeyCode: 46, Key: "Delete"}, - "Home": {KeyCode: 36, Key: "Home"}, - "End": {KeyCode: 35, Key: "End"}, + "PageUp": {Code: "PageUp", KeyCode: 33, Key: "PageUp"}, + "PageDown": {Code: "PageDown", KeyCode: 34, Key: "PageDown"}, + "Insert": {Code: "Insert", KeyCode: 45, Key: "Insert"}, + "Delete": {Code: "Delete", KeyCode: 46, Key: "Delete"}, + "Home": {Code: "Home", KeyCode: 36, Key: "Home"}, + "End": {Code: "End", KeyCode: 35, Key: "End"}, - "ArrowLeft": {KeyCode: 37, Key: "ArrowLeft"}, - "ArrowUp": {KeyCode: 38, Key: "ArrowUp"}, - "ArrowRight": {KeyCode: 39, Key: "ArrowRight"}, - "ArrowDown": {KeyCode: 40, Key: "ArrowDown"}, + "ArrowLeft": {Code: "ArrowLeft", KeyCode: 37, Key: "ArrowLeft"}, + "ArrowUp": {Code: "ArrowUp", KeyCode: 38, Key: "ArrowUp"}, + "ArrowRight": {Code: "ArrowRight", KeyCode: 39, Key: "ArrowRight"}, + "ArrowDown": {Code: "ArrowDown", KeyCode: 40, Key: "ArrowDown"}, // Numpad - "NumLock": {KeyCode: 144, Key: "NumLock"}, - "NumpadDivide": {KeyCode: 111, Key: "/", Location: 3}, - "NumpadMultiply": {KeyCode: 106, Key: "*", Location: 3}, - "NumpadSubtract": {KeyCode: 109, Key: "-", Location: 3}, - "Numpad7": {KeyCode: 36, ShiftKeyCode: 103, Key: "Home", ShiftKey: "7", Location: 3}, - "Numpad8": {KeyCode: 38, ShiftKeyCode: 104, Key: "ArrowUp", ShiftKey: "8", Location: 3}, - "Numpad9": {KeyCode: 33, ShiftKeyCode: 105, Key: "PageUp", ShiftKey: "9", Location: 3}, - "Numpad4": {KeyCode: 37, ShiftKeyCode: 100, Key: "ArrowLeft", ShiftKey: "4", Location: 3}, - "Numpad5": {KeyCode: 12, ShiftKeyCode: 101, Key: "Clear", ShiftKey: "5", Location: 3}, - "Numpad6": {KeyCode: 39, ShiftKeyCode: 102, Key: "ArrowRight", ShiftKey: "6", Location: 3}, - "NumpadAdd": {KeyCode: 107, Key: "+", Location: 3}, - "Numpad1": {KeyCode: 35, ShiftKeyCode: 97, Key: "End", ShiftKey: "1", Location: 3}, - "Numpad2": {KeyCode: 40, ShiftKeyCode: 98, Key: "ArrowDown", ShiftKey: "2", Location: 3}, - "Numpad3": {KeyCode: 34, ShiftKeyCode: 99, Key: "PageDown", ShiftKey: "3", Location: 3}, - "Numpad0": {KeyCode: 45, ShiftKeyCode: 96, Key: "Insert", ShiftKey: "0", Location: 3}, - "NumpadDecimal": {KeyCode: 46, ShiftKeyCode: 110, Key: "\u0000", ShiftKey: ".", Location: 3}, - "NumpadEnter": {KeyCode: 13, Key: "Enter", Text: "\r", Location: 3}, + "NumLock": {Code: "NumLock", KeyCode: 144, Key: "NumLock"}, + "NumpadDivide": {Code: "NumpadDivide", KeyCode: 111, Key: "/", Location: 3}, + "NumpadMultiply": {Code: "NumpadMultiply", KeyCode: 106, Key: "*", Location: 3}, + "NumpadSubtract": {Code: "NumpadSubtract", KeyCode: 109, Key: "-", Location: 3}, + "Numpad7": {Code: "Numpad7", KeyCode: 36, ShiftKeyCode: 103, Key: "Home", ShiftKey: "7", Location: 3}, + "Numpad8": {Code: "Numpad8", KeyCode: 38, ShiftKeyCode: 104, Key: "ArrowUp", ShiftKey: "8", Location: 3}, + "Numpad9": {Code: "Numpad9", KeyCode: 33, ShiftKeyCode: 105, Key: "PageUp", ShiftKey: "9", Location: 3}, + "Numpad4": {Code: "Numpad4", KeyCode: 37, ShiftKeyCode: 100, Key: "ArrowLeft", ShiftKey: "4", Location: 3}, + "Numpad5": {Code: "Numpad5", KeyCode: 12, ShiftKeyCode: 101, Key: "Clear", ShiftKey: "5", Location: 3}, + "Numpad6": {Code: "Numpad6", KeyCode: 39, ShiftKeyCode: 102, Key: "ArrowRight", ShiftKey: "6", Location: 3}, + "NumpadAdd": {Code: "NumpadAdd", KeyCode: 107, Key: "+", Location: 3}, + "Numpad1": {Code: "Numpad1", KeyCode: 35, ShiftKeyCode: 97, Key: "End", ShiftKey: "1", Location: 3}, + "Numpad2": {Code: "Numpad2", KeyCode: 40, ShiftKeyCode: 98, Key: "ArrowDown", ShiftKey: "2", Location: 3}, + "Numpad3": {Code: "Numpad3", KeyCode: 34, ShiftKeyCode: 99, Key: "PageDown", ShiftKey: "3", Location: 3}, + "Numpad0": {Code: "Numpad0", KeyCode: 45, ShiftKeyCode: 96, Key: "Insert", ShiftKey: "0", Location: 3}, + "NumpadDecimal": {Code: "NumpadDecimal", KeyCode: 46, ShiftKeyCode: 110, Key: "\u0000", ShiftKey: ".", Location: 3}, + "NumpadEnter": {Code: "NumpadEnter", KeyCode: 13, Key: "Enter", Text: "\r", Location: 3}, } register("us", validKeys, Keys) diff --git a/vendor/github.com/grafana/xk6-browser/log/logger.go b/vendor/github.com/grafana/xk6-browser/log/logger.go index 722aae96373..374e26c7266 100644 --- a/vendor/github.com/grafana/xk6-browser/log/logger.go +++ b/vendor/github.com/grafana/xk6-browser/log/logger.go @@ -31,21 +31,21 @@ func NewNullLogger() *Logger { } // New creates a new logger. -func New(logger *logrus.Logger, iterID string) *Logger { - var defLogger bool - if logger == nil { - defLogger = true - logger = logrus.New() - } - l := &Logger{ - Logger: logger, +func New(logger logrus.FieldLogger, iterID string) *Logger { + ll := &Logger{ + Logger: logrus.New(), iterID: iterID, } - if defLogger { - l.Warnf("Logger", "no logger supplied, using default") + + if logger == nil { + ll.Warnf("Logger", "no logger supplied, using default") + } else if l, ok := logger.(*logrus.Logger); !ok { + ll.Warnf("Logger", "invalid logger type %T, using default", logger) + } else { + ll.Logger = l } - return l + return ll } // Tracef logs a trace message. diff --git a/vendor/modules.txt b/vendor/modules.txt index 702b953a588..173c11f20d1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -138,13 +138,14 @@ github.com/golang/snappy # github.com/gorilla/websocket v1.5.0 ## explicit; go 1.12 github.com/gorilla/websocket -# github.com/grafana/xk6-browser v0.8.1-0.20230207135343-cfd6a83dfc42 +# github.com/grafana/xk6-browser v0.8.2-0.20230329135657-a01218eaee2f ## explicit; go 1.19 github.com/grafana/xk6-browser/api github.com/grafana/xk6-browser/browser github.com/grafana/xk6-browser/chromium github.com/grafana/xk6-browser/common github.com/grafana/xk6-browser/common/js +github.com/grafana/xk6-browser/k6error github.com/grafana/xk6-browser/k6ext github.com/grafana/xk6-browser/keyboardlayout github.com/grafana/xk6-browser/log