From 8457c91728bcdb3710628d27b8110be14fa73cfa Mon Sep 17 00:00:00 2001 From: Ankur Date: Fri, 29 Sep 2023 10:55:12 +0100 Subject: [PATCH] Upgrade xk6-browser to v1.1.0 (#3355) --- go.mod | 2 +- go.sum | 4 +- .../xk6-browser/api/browser_context.go | 42 ++- .../xk6-browser/api/console_message.go | 19 ++ .../grafana/xk6-browser/api/page.go | 1 + .../grafana/xk6-browser/browser/mapping.go | 60 +++- .../grafana/xk6-browser/common/barrier.go | 2 +- .../grafana/xk6-browser/common/browser.go | 16 + .../xk6-browser/common/browser_context.go | 284 ++++++++++++++---- .../xk6-browser/common/element_handle.go | 2 +- .../grafana/xk6-browser/common/frame.go | 10 +- .../xk6-browser/common/frame_session.go | 13 + .../js/{web_vital.go => embedded_scripts.go} | 7 + .../xk6-browser/common/js/k6_object.js | 1 + .../grafana/xk6-browser/common/page.go | 255 ++++++++++++++-- .../grafana/xk6-browser/common/timeout.go | 18 +- .../github.com/grafana/xk6-browser/env/env.go | 30 +- vendor/modules.txt | 2 +- 18 files changed, 656 insertions(+), 112 deletions(-) create mode 100644 vendor/github.com/grafana/xk6-browser/api/console_message.go rename vendor/github.com/grafana/xk6-browser/common/js/{web_vital.go => embedded_scripts.go} (73%) create mode 100644 vendor/github.com/grafana/xk6-browser/common/js/k6_object.js diff --git a/go.mod b/go.mod index 82a82e389ed..f309e37ce3b 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.3 github.com/gorilla/websocket v1.5.0 - github.com/grafana/xk6-browser v1.0.2 + github.com/grafana/xk6-browser v1.1.0 github.com/grafana/xk6-grpc v0.1.4-0.20230919144024-6ed5daf33509 github.com/grafana/xk6-output-prometheus-remote v0.3.0 github.com/grafana/xk6-redis v0.1.1 diff --git a/go.sum b/go.sum index afbfd2dc1ab..2a8f907d93b 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 v1.0.2 h1:B9ll8xLH68hfCBy3sTzhmksCxwgJBIcqgPeX3mht6jM= -github.com/grafana/xk6-browser v1.0.2/go.mod h1:LV/ECGBCN3vRN/A4St+Ep9JUpbKJuRsj+6TBihQptGw= +github.com/grafana/xk6-browser v1.1.0 h1:QHXF0zqm/MgUuJXfIBi9xUbJy5Z0molSWcYgtMkLbk8= +github.com/grafana/xk6-browser v1.1.0/go.mod h1:D0ybXLDFAnIV+fNFvaQY8LIXy7OqobLA3f810BkUVXU= github.com/grafana/xk6-grpc v0.1.4-0.20230919144024-6ed5daf33509 h1:9ujE4S5cA3WDhRJnwNuUDtfk3w9FeWx6PaZ+lb3o46M= github.com/grafana/xk6-grpc v0.1.4-0.20230919144024-6ed5daf33509/go.mod h1:sFTwAsHAtp2f1PNiq0wPjJ7HrAIKploI7Y5mOYo+zIQ= github.com/grafana/xk6-output-prometheus-remote v0.3.0 h1:uALRRqFGF7hy75Vc0k6TAj04d9x2kt460t7RZirhxjc= 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 bef11b173db..d0bfb362ec9 100644 --- a/vendor/github.com/grafana/xk6-browser/api/browser_context.go +++ b/vendor/github.com/grafana/xk6-browser/api/browser_context.go @@ -6,13 +6,13 @@ import ( // BrowserContext is the public interface of a CDP browser context. type BrowserContext interface { - AddCookies(cookies goja.Value) + AddCookies(cookies []*Cookie) error AddInitScript(script goja.Value, arg goja.Value) error Browser() Browser - ClearCookies() + ClearCookies() error ClearPermissions() Close() - Cookies() ([]any, error) // TODO: make it []Cookie later on + Cookies(urls ...string) ([]*Cookie, error) ExposeBinding(name string, callback goja.Callable, opts goja.Value) ExposeFunction(name string, callback goja.Callable) GrantPermissions(permissions []string, opts goja.Value) @@ -36,3 +36,39 @@ type BrowserContext interface { Unroute(url goja.Value, handler goja.Callable) WaitForEvent(event string, optsOrPredicate goja.Value) any } + +// Cookie represents a browser cookie. +// +// https://datatracker.ietf.org/doc/html/rfc6265. +type Cookie struct { + Name string `js:"name" json:"name"` // Cookie name. + Value string `js:"value" json:"value"` // Cookie value. + Domain string `js:"domain" json:"domain"` // Cookie domain. + Path string `js:"path" json:"path"` // Cookie path. + HTTPOnly bool `js:"httpOnly" json:"httpOnly"` // True if cookie is http-only. + Secure bool `js:"secure" json:"secure"` // True if cookie is secure. + SameSite CookieSameSite `js:"sameSite" json:"sameSite"` // Cookie SameSite type. + URL string `js:"url" json:"url,omitempty"` // Cookie URL. + // Cookie expiration date as the number of seconds since the UNIX epoch. + Expires int64 `js:"expires" json:"expires"` +} + +// CookieSameSite represents the cookie's 'SameSite' status. +// +// https://tools.ietf.org/html/draft-west-first-party-cookies. +type CookieSameSite string + +const ( + // CookieSameSiteStrict sets the cookie to be sent only in a first-party + // context and not be sent along with requests initiated by third party + // websites. + CookieSameSiteStrict CookieSameSite = "Strict" + + // CookieSameSiteLax sets the cookie to be sent along with "same-site" + // requests, and with "cross-site" top-level navigations. + CookieSameSiteLax CookieSameSite = "Lax" + + // CookieSameSiteNone sets the cookie to be sent in all contexts, i.e + // potentially insecure third-party requests. + CookieSameSiteNone CookieSameSite = "None" +) diff --git a/vendor/github.com/grafana/xk6-browser/api/console_message.go b/vendor/github.com/grafana/xk6-browser/api/console_message.go new file mode 100644 index 00000000000..b7190b1f543 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/api/console_message.go @@ -0,0 +1,19 @@ +package api + +// ConsoleMessage represents a page console message. +type ConsoleMessage struct { + // Args represent the list of arguments passed to a console function call. + Args []JSHandle + + // Page is the page that produced the console message, if any. + Page Page + + // Text represents the text of the console message. + Text string + + // Type is the type of the console message. + // It can be one of 'log', 'debug', 'info', 'error', 'warning', 'dir', 'dirxml', + // 'table', 'trace', 'clear', 'startGroup', 'startGroupCollapsed', 'endGroup', + // 'assert', 'profile', 'profileEnd', 'count', 'timeEnd'. + Type string +} diff --git a/vendor/github.com/grafana/xk6-browser/api/page.go b/vendor/github.com/grafana/xk6-browser/api/page.go index 37580839b87..e4f50057f4a 100644 --- a/vendor/github.com/grafana/xk6-browser/api/page.go +++ b/vendor/github.com/grafana/xk6-browser/api/page.go @@ -47,6 +47,7 @@ type Page interface { // Locator creates and returns a new locator for this page (main frame). Locator(selector string, opts goja.Value) Locator MainFrame() Frame + On(event string, handler func(*ConsoleMessage) error) error Opener() Page Pause() Pdf(opts goja.Value) []byte diff --git a/vendor/github.com/grafana/xk6-browser/browser/mapping.go b/vendor/github.com/grafana/xk6-browser/browser/mapping.go index 2ce97453dff..597b10b354c 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/mapping.go +++ b/vendor/github.com/grafana/xk6-browser/browser/mapping.go @@ -506,7 +506,15 @@ func mapPage(vu moduleVU, p api.Page) mapping { mf := mapFrame(vu, p.MainFrame()) return rt.ToValue(mf).ToObject(rt) }, - "mouse": rt.ToValue(p.GetMouse()).ToObject(rt), + "mouse": rt.ToValue(p.GetMouse()).ToObject(rt), + "on": func(event string, handler goja.Callable) error { + mapMsgAndHandleEvent := func(m *api.ConsoleMessage) error { + mapping := mapConsoleMessage(vu, m) + _, err := handler(goja.Undefined(), vu.Runtime().ToValue(mapping)) + return err + } + return p.On(event, mapMsgAndHandleEvent) //nolint:wrapcheck + }, "opener": p.Opener, "pause": p.Pause, "pdf": p.Pdf, @@ -613,18 +621,13 @@ 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": func() ([]any, error) { - cc, err := bc.Cookies() - ctx := vu.Context() - panicIfFatalError(ctx, err) - return cc, err //nolint:wrapcheck - }, + "addCookies": bc.AddCookies, + "addInitScript": bc.AddInitScript, + "browser": bc.Browser, + "clearCookies": bc.ClearCookies, + "clearPermissions": bc.ClearPermissions, + "close": bc.Close, + "cookies": bc.Cookies, "exposeBinding": bc.ExposeBinding, "exposeFunction": bc.ExposeFunction, "grantPermissions": bc.GrantPermissions, @@ -671,6 +674,37 @@ func mapBrowserContext(vu moduleVU, bc api.BrowserContext) mapping { } } +// mapConsoleMessage to the JS module. +func mapConsoleMessage(vu moduleVU, cm *api.ConsoleMessage) mapping { + rt := vu.Runtime() + return mapping{ + "args": func() *goja.Object { + var ( + margs []mapping + args = cm.Args + ) + for _, arg := range args { + a := mapJSHandle(vu, arg) + margs = append(margs, a) + } + + return rt.ToValue(margs).ToObject(rt) + }, + // page(), text() and type() are defined as + // functions in order to match Playwright's API + "page": func() *goja.Object { + mp := mapPage(vu, cm.Page) + return rt.ToValue(mp).ToObject(rt) + }, + "text": func() *goja.Object { + return rt.ToValue(cm.Text).ToObject(rt) + }, + "type": func() *goja.Object { + return rt.ToValue(cm.Type).ToObject(rt) + }, + } +} + // mapBrowser to the JS module. func mapBrowser(vu moduleVU) mapping { rt := vu.Runtime() diff --git a/vendor/github.com/grafana/xk6-browser/common/barrier.go b/vendor/github.com/grafana/xk6-browser/common/barrier.go index 83c66df2dd3..e10424b5902 100644 --- a/vendor/github.com/grafana/xk6-browser/common/barrier.go +++ b/vendor/github.com/grafana/xk6-browser/common/barrier.go @@ -32,7 +32,7 @@ func (b *Barrier) AddFrameNavigation(frame *Frame) { atomic.AddInt64(&b.count, 1) select { case <-frame.ctx.Done(): - case <-time.After(time.Duration(frame.manager.timeoutSettings.navigationTimeout()) * time.Second): + case <-time.After(frame.manager.timeoutSettings.navigationTimeout()): b.errCh <- ErrTimedOut case <-ch: b.ch <- true diff --git a/vendor/github.com/grafana/xk6-browser/common/browser.go b/vendor/github.com/grafana/xk6-browser/common/browser.go index a0a0598c09c..13c1e902398 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser.go @@ -47,6 +47,16 @@ type Browser struct { // A *Connection is saved to this field, see: connect(). conn connection + // This mutex is only needed in an edge case where we have multiple + // instances of k6 connecting to the same chrome instance. In this + // case when a page is created by the first k6 instance, the second + // instance of k6 will also receive an onAttachedToTarget event. When + // this occurs there's a small chance that at the same time a new + // context is being created by the second k6 instance. So the read + // occurs in getDefaultBrowserContextOrMatchedID which is called by + // onAttachedToTarget, and the write in NewContext. This mutex protects + // the read/write race condition for this one case. + contextMu sync.RWMutex context *BrowserContext defaultContext *BrowserContext @@ -139,6 +149,9 @@ func (b *Browser) disposeContext(id cdp.BrowserContextID) error { // getDefaultBrowserContextOrMatchedID returns the BrowserContext for the given browser context ID. // If the browser context is not found, the default BrowserContext is returned. func (b *Browser) getDefaultBrowserContextOrMatchedID(id cdp.BrowserContextID) *BrowserContext { + b.contextMu.RLock() + defer b.contextMu.RUnlock() + if b.context == nil || b.context.id != id { return b.defaultContext } @@ -512,6 +525,9 @@ func (b *Browser) NewContext(opts goja.Value) (api.BrowserContext, error) { if err != nil { return nil, fmt.Errorf("new context: %w", err) } + + b.contextMu.Lock() + defer b.contextMu.Unlock() b.context = browserCtx return browserCtx, nil 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 82a4540b8fe..640fe5ab7f1 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser_context.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser_context.go @@ -3,7 +3,9 @@ package common import ( "context" "fmt" + "net/url" "reflect" + "strings" "time" "github.com/grafana/xk6-browser/api" @@ -66,9 +68,13 @@ func NewBrowserContext( } rt := b.vu.Runtime() + k6Obj := rt.ToValue(js.K6ObjectScript) wv := rt.ToValue(js.WebVitalIIFEScript) wvi := rt.ToValue(js.WebVitalInitScript) + if err := b.AddInitScript(k6Obj, nil); err != nil { + return nil, fmt.Errorf("adding k6 object to new browser context: %w", err) + } if err := b.AddInitScript(wv, nil); err != nil { return nil, fmt.Errorf("adding web vital script to new browser context: %w", err) } @@ -79,17 +85,6 @@ func NewBrowserContext( return &b, nil } -// AddCookies adds cookies into this browser context. -// All pages within this context will have these cookies installed. -func (b *BrowserContext) AddCookies(cookies goja.Value) { - 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) error { b.logger.Debugf("BrowserContext:AddInitScript", "bctxid:%v", b.id) @@ -145,16 +140,6 @@ func (b *BrowserContext) Browser() api.Browser { return b.browser } -// ClearCookies clears cookies. -func (b *BrowserContext) ClearCookies() { - b.logger.Debugf("BrowserContext:ClearCookies", "bctxid:%v", b.id) - - action := storage.ClearCookies().WithBrowserContextID(b.id) - if err := action.Do(b.ctx); err != nil { - k6ext.Panic(b.ctx, "clearing cookies: %w", err) - } -} - // ClearPermissions clears any permission overrides. func (b *BrowserContext) ClearPermissions() { b.logger.Debugf("BrowserContext:ClearPermissions", "bctxid:%v", b.id) @@ -177,11 +162,6 @@ func (b *BrowserContext) Close() { } } -// Cookies is not implemented. -func (b *BrowserContext) Cookies() ([]any, error) { - return nil, fmt.Errorf("BrowserContext.cookies() has not been implemented yet: %w", k6error.ErrFatal) -} - // ExposeBinding is not implemented. func (b *BrowserContext) ExposeBinding(name string, callback goja.Callable, opts goja.Value) { k6ext.Panic(b.ctx, "BrowserContext.exposeBinding(name, callback, opts) has not been implemented yet") @@ -285,14 +265,14 @@ func (b *BrowserContext) Route(url goja.Value, handler goja.Callable) { func (b *BrowserContext) SetDefaultNavigationTimeout(timeout int64) { b.logger.Debugf("BrowserContext:SetDefaultNavigationTimeout", "bctxid:%v timeout:%d", b.id, timeout) - b.timeoutSettings.setDefaultNavigationTimeout(timeout) + b.timeoutSettings.setDefaultNavigationTimeout(time.Duration(timeout) * time.Millisecond) } // SetDefaultTimeout sets the default maximum timeout in milliseconds. func (b *BrowserContext) SetDefaultTimeout(timeout int64) { b.logger.Debugf("BrowserContext:SetDefaultTimeout", "bctxid:%v timeout:%d", b.id, timeout) - b.timeoutSettings.setDefaultTimeout(timeout) + b.timeoutSettings.setDefaultTimeout(time.Duration(timeout) * time.Millisecond) } // SetExtraHTTPHeaders is not implemented. @@ -463,48 +443,236 @@ 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") +// AddCookies adds cookies into this browser context. +// All pages within this context will have these cookies installed. +func (b *BrowserContext) AddCookies(cookies []*api.Cookie) error { + b.logger.Debugf("BrowserContext:AddCookies", "bctxid:%v", b.id) + + // skip work if no cookies provided. + if len(cookies) == 0 { + return fmt.Errorf("no cookies provided") } - rt := b.vu.Runtime() - err := rt.ExportTo(cookies, &cookieParams) - if err != nil { - return fmt.Errorf("unable to export cookies value to cookieParams. %w", err) + cookiesToSet := make([]*network.CookieParam, 0, len(cookies)) + for _, c := range cookies { + if c.Name == "" { + return fmt.Errorf("cookie name must be set: %#v", c) + } + if c.Value == "" { + return fmt.Errorf("cookie value must be set: %#v", c) + } + // if URL is not set, both Domain and Path must be provided + if c.URL == "" && (c.Domain == "" || c.Path == "") { + const msg = "if cookie URL is not provided, both domain and path must be specified: %#v" + return fmt.Errorf(msg, c) + } + // calculate the cookie expiration date, session cookie if not set. + var ts *cdp.TimeSinceEpoch + if c.Expires > 0 { + t := cdp.TimeSinceEpoch(time.Unix(c.Expires, 0)) + ts = &t + } + cookiesToSet = append(cookiesToSet, &network.CookieParam{ + Name: c.Name, + Value: c.Value, + Domain: c.Domain, + Path: c.Path, + URL: c.URL, + Expires: ts, + HTTPOnly: c.HTTPOnly, + Secure: c.Secure, + SameSite: network.CookieSameSite(c.SameSite), + }) + } + + setCookies := storage. + SetCookies(cookiesToSet). + WithBrowserContextID(b.id) + if err := setCookies.Do(cdp.WithExecutor(b.ctx, b.browser.conn)); err != nil { + return fmt.Errorf("cannot set cookies: %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] + return nil +} - if cookieParam.Name == "" { - return fmt.Errorf("cookie name is not set. %#v", cookieParam) - } +// ClearCookies clears cookies. +func (b *BrowserContext) ClearCookies() error { + b.logger.Debugf("BrowserContext:ClearCookies", "bctxid:%v", b.id) + + clearCookies := storage. + ClearCookies(). + WithBrowserContextID(b.id) + if err := clearCookies.Do(cdp.WithExecutor(b.ctx, b.browser.conn)); err != nil { + return fmt.Errorf("clearing cookies: %w", err) + } + return nil +} - if cookieParam.Value == "" { - return fmt.Errorf("cookie value is not set. %#v", cookieParam) +// Cookies returns all cookies. +// Some of them can be added with the AddCookies method and some of them are +// automatically taken from the browser context when it is created. And some of +// them are set by the page, i.e., using the Set-Cookie HTTP header or via +// JavaScript like document.cookie. +func (b *BrowserContext) Cookies(urls ...string) ([]*api.Cookie, error) { + b.logger.Debugf("BrowserContext:Cookies", "bctxid:%v", b.id) + + // get cookies from this browser context. + getCookies := storage. + GetCookies(). + WithBrowserContextID(b.id) + networkCookies, err := getCookies.Do( + cdp.WithExecutor(b.ctx, b.browser.conn), + ) + if err != nil { + return nil, fmt.Errorf("retrieving cookies: %w", err) + } + // return if no cookies found so we don't have to needlessly convert them. + // users can still work with cookies using the empty slice. + // like this: cookies.length === 0. + if len(networkCookies) == 0 { + return nil, nil + } + + // convert the received CDP cookies to the browser API format. + cookies := make([]*api.Cookie, len(networkCookies)) + for i, c := range networkCookies { + cookies[i] = &api.Cookie{ + Name: c.Name, + Value: c.Value, + Domain: c.Domain, + Path: c.Path, + Expires: int64(c.Expires), + HTTPOnly: c.HTTPOnly, + Secure: c.Secure, + SameSite: api.CookieSameSite(c.SameSite), } + } + // filter cookies by the provided URLs. + cookies, err = filterCookies(cookies, urls...) + if err != nil { + return nil, fmt.Errorf("filtering cookies: %w", err) + } + if len(cookies) == 0 { + return nil, nil + } - // 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, - ) + return cookies, nil +} + +// filterCookies filters the given cookies based on URLs. +// If an error occurs while parsing the cookie URLs, the error is returned. +func filterCookies(cookies []*api.Cookie, urls ...string) ([]*api.Cookie, error) { + if len(urls) == 0 || len(cookies) == 0 { + return cookies, nil + } + + purls, err := parseURLs(urls...) + if err != nil { + return nil, fmt.Errorf("parsing urls: %w", err) + } + + // the following algorithm is like a sorting algorithm, + // but instead of sorting, it filters the cookies slice + // in place, without allocating a new slice. this is + // done to avoid unnecessary allocations and copying + // of data. + // + // n is used to remember the last cookie that should be + // kept in the cookies slice. all cookies before n should + // be kept, all cookies after n should be removed. it + // constantly shifts cookies to be kept to the left in the + // slice, overwriting cookies that should be removed. + // + // if a cookie should not be kept, it will be overwritten + // by the next cookie that should be kept. if no cookies + // should be kept, a nil slice is returned. otherwise, + // the slice is truncated to the last cookie that should + // be kept. + + var n int + + for _, c := range cookies { + var keep bool + + for _, uri := range purls { + if shouldKeepCookie(c, uri) { + keep = true + break } } - - cookieParamsPointers = append(cookieParamsPointers, &cookieParam) + if !keep { + continue + } + cookies[n] = c + n++ + } + // if no cookies should be kept, return nil instead of + // an empty slice to conform with the API error behavior. + // also makes tests concise. + if n == 0 { + return nil, nil } - 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) + // remove all cookies after the last cookie that should be kept. + return cookies[:n], nil +} + +// shouldKeepCookie determines whether a cookie should be kept, +// based on its compatibility with a specific URL. +// Returns true if the cookie should be kept, false otherwise. +func shouldKeepCookie(c *api.Cookie, uri *url.URL) bool { + // Ensure consistent domain formatting for easier comparison. + // A leading dot means the cookie is valid across subdomains. + // For example, if the domain is example.com, then adding a + // dot turns it into .example.com, making the cookie valid + // for sub.example.com, another.example.com, etc. + domain := c.Domain + if !strings.HasPrefix(domain, ".") { + domain = "." + domain + } + // Confirm that the cookie's domain is a suffix of the URL's + // hostname, emulating how a browser would scope cookies to + // specific domains. + if !strings.HasSuffix(domain, "."+uri.Hostname()) { + return false + } + // Follow RFC 6265 for cookies: an empty or missing path should + // be treated as "/". + // + // See: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4 + path := c.Path + if path == "" { + path = "/" + } + // Ensure that the cookie applies to the specific path of the + // URL, emulating how a browser would scope cookies to specific + // paths within a domain. + if !strings.HasPrefix(path, uri.Path) { + return false + } + // Emulate browser behavior: Don't include secure cookies when + // the scheme is not HTTPS, unless it's localhost. + if uri.Scheme != "https" && uri.Hostname() != "localhost" && c.Secure { + return false + } + + // Keep the cookie. + return true +} + +// parseURLs parses the given URLs. +// If an error occurs while parsing a URL, the error is returned. +func parseURLs(urls ...string) ([]*url.URL, error) { + purls := make([]*url.URL, len(urls)) + for i, u := range urls { + uri, err := url.ParseRequestURI( + strings.TrimSpace(u), + ) + if err != nil { + return nil, fmt.Errorf("%q: %w", u, err) + } + purls[i] = uri } - return nil + return purls, 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 05f4be0ca5b..ae7ca18da45 100644 --- a/vendor/github.com/grafana/xk6-browser/common/element_handle.go +++ b/vendor/github.com/grafana/xk6-browser/common/element_handle.go @@ -249,7 +249,7 @@ func (h *ElementHandle) dblClick(p *Position, opts *MouseClickOptions) error { } func (h *ElementHandle) defaultTimeout() time.Duration { - return time.Duration(h.frame.manager.timeoutSettings.timeout()) * time.Second + return h.frame.manager.timeoutSettings.timeout() } func (h *ElementHandle) dispatchEvent(_ context.Context, typ string, eventInit goja.Value) (any, error) { diff --git a/vendor/github.com/grafana/xk6-browser/common/frame.go b/vendor/github.com/grafana/xk6-browser/common/frame.go index dc1bfbfa22d..85fe0b09436 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame.go @@ -198,7 +198,7 @@ func (f *Frame) detach() { } func (f *Frame) defaultTimeout() time.Duration { - return time.Duration(f.manager.timeoutSettings.timeout()) * time.Second + return f.manager.timeoutSettings.timeout() } func (f *Frame) document() (*ElementHandle, error) { @@ -883,7 +883,7 @@ func (f *Frame) Goto(url string, opts goja.Value) (api.Response, error) { defaultReferer = netMgr.extraHTTPHeaders["referer"] parsedOpts = NewFrameGotoOptions( defaultReferer, - time.Duration(f.manager.timeoutSettings.navigationTimeout())*time.Second, + f.manager.timeoutSettings.navigationTimeout(), ) ) if err := parsedOpts.Parse(f.ctx, opts); err != nil { @@ -1428,7 +1428,9 @@ func (f *Frame) selectOption(selector string, values goja.Value, opts *FrameSele func (f *Frame) SetContent(html string, opts goja.Value) { f.log.Debugf("Frame:SetContent", "fid:%s furl:%q", f.ID(), f.URL()) - parsedOpts := NewFrameSetContentOptions(f.defaultTimeout()) + parsedOpts := NewFrameSetContentOptions( + f.manager.timeoutSettings.navigationTimeout(), + ) if err := parsedOpts.Parse(f.ctx, opts); err != nil { k6ext.Panic(f.ctx, "parsing set content options: %w", err) } @@ -1738,7 +1740,7 @@ func (f *Frame) WaitForNavigation(opts goja.Value) (api.Response, error) { "fid:%s furl:%s", f.ID(), f.URL()) parsedOpts := NewFrameWaitForNavigationOptions( - time.Duration(f.manager.timeoutSettings.timeout()) * time.Second) + f.manager.timeoutSettings.timeout()) if err := parsedOpts.Parse(f.ctx, opts); err != nil { k6ext.Panic(f.ctx, "parsing wait for navigation options: %w", err) } 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 fa954b88d18..fee0bb6d97f 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame_session.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame_session.go @@ -1084,3 +1084,16 @@ func (fs *FrameSession) updateViewport() error { return nil } + +func (fs *FrameSession) executionContextForID( + executionContextID cdpruntime.ExecutionContextID, +) (*ExecutionContext, error) { + fs.contextIDToContextMu.Lock() + defer fs.contextIDToContextMu.Unlock() + + if exc, ok := fs.contextIDToContext[executionContextID]; ok { + return exc, nil + } + + return nil, fmt.Errorf("no execution context found for id: %v", executionContextID) +} diff --git a/vendor/github.com/grafana/xk6-browser/common/js/web_vital.go b/vendor/github.com/grafana/xk6-browser/common/js/embedded_scripts.go similarity index 73% rename from vendor/github.com/grafana/xk6-browser/common/js/web_vital.go rename to vendor/github.com/grafana/xk6-browser/common/js/embedded_scripts.go index e31da9ac76c..66bf5fd6b4e 100644 --- a/vendor/github.com/grafana/xk6-browser/common/js/web_vital.go +++ b/vendor/github.com/grafana/xk6-browser/common/js/embedded_scripts.go @@ -17,3 +17,10 @@ var WebVitalIIFEScript string // //go:embed web_vital_init.js var WebVitalInitScript string + +// K6ObjectScript is used to propagate +// information to other libraries about +// the current user session. +// +//go:embed k6_object.js +var K6ObjectScript string diff --git a/vendor/github.com/grafana/xk6-browser/common/js/k6_object.js b/vendor/github.com/grafana/xk6-browser/common/js/k6_object.js new file mode 100644 index 00000000000..fbf489b1fd8 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/common/js/k6_object.js @@ -0,0 +1 @@ +window.k6 = {}; diff --git a/vendor/github.com/grafana/xk6-browser/common/page.go b/vendor/github.com/grafana/xk6-browser/common/page.go index 40df9f0d404..98b5a05ccc5 100644 --- a/vendor/github.com/grafana/xk6-browser/common/page.go +++ b/vendor/github.com/grafana/xk6-browser/common/page.go @@ -8,23 +8,30 @@ import ( "sync" "time" - "github.com/grafana/xk6-browser/api" - "github.com/grafana/xk6-browser/k6ext" - "github.com/grafana/xk6-browser/log" - - k6modules "go.k6.io/k6/js/modules" - + "github.com/chromedp/cdproto" "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" + cdpruntime "github.com/chromedp/cdproto/runtime" "github.com/chromedp/cdproto/target" "github.com/dop251/goja" + "github.com/mstoykov/k6-taskqueue-lib/taskqueue" + + "github.com/grafana/xk6-browser/api" + "github.com/grafana/xk6-browser/k6ext" + "github.com/grafana/xk6-browser/log" + + k6modules "go.k6.io/k6/js/modules" ) -const webVitalBinding = "k6browserSendWebVitalMetric" +const ( + webVitalBinding = "k6browserSendWebVitalMetric" + + eventPageConsoleAPICalled = "console" +) // Ensure page implements the EventEmitter, Target and Page interfaces. var ( @@ -32,6 +39,8 @@ var ( _ api.Page = &Page{} ) +type consoleEventHandlerFunc func(*api.ConsoleMessage) error + // Page stores Page/tab related context. type Page struct { BaseEventEmitter @@ -69,12 +78,18 @@ type Page struct { backgroundPage bool + eventCh chan Event + eventHandlers map[string][]consoleEventHandlerFunc + eventHandlersMu sync.RWMutex + mainFrameSession *FrameSession - // TODO: FrameSession changes by attachFrameSession (mutex?) - frameSessions map[cdp.FrameID]*FrameSession - workers map[target.SessionID]*Worker - routes []api.Route - vu k6modules.VU + frameSessions map[cdp.FrameID]*FrameSession + frameSessionsMu sync.RWMutex + workers map[target.SessionID]*Worker + routes []api.Route + vu k6modules.VU + + tq *taskqueue.TaskQueue logger *log.Logger } @@ -104,6 +119,8 @@ func NewPage( timeoutSettings: NewTimeoutSettings(bctx.timeoutSettings), Keyboard: NewKeyboard(ctx, s), jsEnabled: true, + eventCh: make(chan Event), + eventHandlers: make(map[string][]consoleEventHandlerFunc), frameSessions: make(map[cdp.FrameID]*FrameSession), workers: make(map[target.SessionID]*Worker), routes: make([]api.Route, 0), @@ -121,7 +138,7 @@ func NewPage( } var err error - p.frameManager = NewFrameManager(ctx, s, &p, bctx.timeoutSettings, p.logger) + p.frameManager = NewFrameManager(ctx, s, &p, p.timeoutSettings, p.logger) p.mainFrameSession, err = NewFrameSession(ctx, s, &p, nil, tid, p.logger) if err != nil { p.logger.Debugf("Page:NewPage:NewFrameSession:return", "sid:%v tid:%v err:%v", @@ -129,10 +146,14 @@ func NewPage( return nil, err } + p.frameSessionsMu.Lock() p.frameSessions[cdp.FrameID(tid)] = p.mainFrameSession + p.frameSessionsMu.Unlock() p.Mouse = NewMouse(ctx, s, p.frameManager.MainFrame(), bctx.timeoutSettings, p.Keyboard) p.Touchscreen = NewTouchscreen(ctx, s, p.Keyboard) + p.initEvents() + action := target.SetAutoAttach(true, true).WithFlatten(true) if err := action.Do(cdp.WithExecutor(p.ctx, p.session)); err != nil { return nil, fmt.Errorf("internal error while auto attaching to browser pages: %w", err) @@ -150,6 +171,48 @@ func NewPage( return &p, nil } +func (p *Page) initEvents() { + p.logger.Debugf("Page:initEvents", + "sid:%v tid:%v", p.session.ID(), p.targetID) + + events := []string{ + cdproto.EventRuntimeConsoleAPICalled, + } + p.session.on(p.ctx, events, p.eventCh) + + go func() { + p.logger.Debugf("Page:initEvents:go", + "sid:%v tid:%v", p.session.ID(), p.targetID) + defer func() { + p.logger.Debugf("Page:initEvents:go:return", + "sid:%v tid:%v", p.session.ID(), p.targetID) + // TaskQueue is only initialized when calling page.on() method + // so users are not always required to close the page in order + // to let the iteration finish. + if p.tq != nil { + p.tq.Close() + } + }() + + for { + select { + case <-p.session.Done(): + p.logger.Debugf("Page:initEvents:go:session.done", + "sid:%v tid:%v", p.session.ID(), p.targetID) + return + case <-p.ctx.Done(): + p.logger.Debugf("Page:initEvents:go:ctx.Done", + "sid:%v tid:%v", p.session.ID(), p.targetID) + return + case event := <-p.eventCh: + if ev, ok := event.data.(*cdpruntime.EventConsoleAPICalled); ok { + p.onConsoleAPICalled(ev) + } + } + } + }() +} + func (p *Page) closeWorker(sessionID target.SessionID) { p.logger.Debugf("Page:closeWorker", "sid:%v", sessionID) @@ -160,7 +223,7 @@ func (p *Page) closeWorker(sessionID target.SessionID) { } func (p *Page) defaultTimeout() time.Duration { - return time.Duration(p.timeoutSettings.timeout()) * time.Second + return p.timeoutSettings.timeout() } func (p *Page) didClose() { @@ -275,12 +338,15 @@ func (p *Page) getOwnerFrame(apiCtx context.Context, h *ElementHandle) cdp.Frame func (p *Page) attachFrameSession(fid cdp.FrameID, fs *FrameSession) { p.logger.Debugf("Page:attachFrameSession", "sid:%v fid=%v", p.session.ID(), fid) + p.frameSessionsMu.Lock() + defer p.frameSessionsMu.Unlock() fs.page.frameSessions[fid] = fs } func (p *Page) getFrameSession(frameID cdp.FrameID) *FrameSession { p.logger.Debugf("Page:getFrameSession", "sid:%v fid:%v", p.sessionID(), frameID) - + p.frameSessionsMu.RLock() + defer p.frameSessionsMu.RUnlock() return p.frameSessions[frameID] } @@ -320,6 +386,9 @@ func (p *Page) setViewportSize(viewportSize *Size) error { func (p *Page) updateExtraHTTPHeaders() { p.logger.Debugf("Page:updateExtraHTTPHeaders", "sid:%v", p.sessionID()) + p.frameSessionsMu.RLock() + defer p.frameSessionsMu.RUnlock() + for _, fs := range p.frameSessions { fs.updateExtraHTTPHeaders(false) } @@ -328,6 +397,9 @@ func (p *Page) updateExtraHTTPHeaders() { func (p *Page) updateGeolocation() error { p.logger.Debugf("Page:updateGeolocation", "sid:%v", p.sessionID()) + p.frameSessionsMu.RLock() + defer p.frameSessionsMu.RUnlock() + for _, fs := range p.frameSessions { p.logger.Debugf("Page:updateGeolocation:frameSession", "sid:%v tid:%v wid:%v", @@ -341,12 +413,16 @@ func (p *Page) updateGeolocation() error { return err } } + return nil } func (p *Page) updateOffline() { p.logger.Debugf("Page:updateOffline", "sid:%v", p.sessionID()) + p.frameSessionsMu.RLock() + defer p.frameSessionsMu.RUnlock() + for _, fs := range p.frameSessions { fs.updateOffline(false) } @@ -355,6 +431,9 @@ func (p *Page) updateOffline() { func (p *Page) updateHttpCredentials() { p.logger.Debugf("Page:updateHttpCredentials", "sid:%v", p.sessionID()) + p.frameSessionsMu.RLock() + defer p.frameSessionsMu.RUnlock() + for _, fs := range p.frameSessions { fs.updateHTTPCredentials(false) } @@ -503,11 +582,14 @@ func (p *Page) EmulateMedia(opts goja.Value) { p.colorScheme = parsedOpts.ColorScheme p.reducedMotion = parsedOpts.ReducedMotion + p.frameSessionsMu.RLock() for _, fs := range p.frameSessions { if err := fs.updateEmulateMedia(false); err != nil { + p.frameSessionsMu.RUnlock() k6ext.Panic(p.ctx, "emulating media: %w", err) } } + p.frameSessionsMu.RUnlock() applySlowMo(p.ctx) } @@ -712,6 +794,32 @@ func (p *Page) MainFrame() api.Frame { return mf } +// On subscribes to a page event for which the given handler will be executed +// passing in the ConsoleMessage associated with the event. +// The only accepted event value is 'console'. +func (p *Page) On(event string, handler func(*api.ConsoleMessage) error) error { + if event != eventPageConsoleAPICalled { + return fmt.Errorf("unknown page event: %q, must be %q", event, eventPageConsoleAPICalled) + } + + // Once the TaskQueue is initialized, it has to be closed so the event loop can finish. + // Therefore, instead of doing it in the constructor, we initialize it only when page.on() + // is called, so the user is only required to close the page it using this method. + if p.tq == nil { + p.tq = taskqueue.New(p.vu.RegisterCallback) + } + + p.eventHandlersMu.Lock() + defer p.eventHandlersMu.Unlock() + + if _, ok := p.eventHandlers[eventPageConsoleAPICalled]; !ok { + p.eventHandlers[eventPageConsoleAPICalled] = make([]consoleEventHandlerFunc, 0, 1) + } + p.eventHandlers[eventPageConsoleAPICalled] = append(p.eventHandlers[eventPageConsoleAPICalled], handler) + + return nil +} + // Opener returns the opener of the target. func (p *Page) Opener() api.Page { return p.opener @@ -752,7 +860,10 @@ func (p *Page) QueryAll(selector string) ([]api.ElementHandle, error) { func (p *Page) Reload(opts goja.Value) api.Response { p.logger.Debugf("Page:Reload", "sid:%v", p.sessionID()) - parsedOpts := NewPageReloadOptions(LifecycleEventLoad, p.defaultTimeout()) + parsedOpts := NewPageReloadOptions( + LifecycleEventLoad, + p.timeoutSettings.navigationTimeout(), + ) if err := parsedOpts.Parse(p.ctx, opts); err != nil { k6ext.Panic(p.ctx, "parsing reload options: %w", err) } @@ -860,14 +971,14 @@ func (p *Page) SetContent(html string, opts goja.Value) { func (p *Page) SetDefaultNavigationTimeout(timeout int64) { p.logger.Debugf("Page:SetDefaultNavigationTimeout", "sid:%v timeout:%d", p.sessionID(), timeout) - p.timeoutSettings.setDefaultNavigationTimeout(timeout) + p.timeoutSettings.setDefaultNavigationTimeout(time.Duration(timeout) * time.Millisecond) } // SetDefaultTimeout sets the default maximum timeout in milliseconds. func (p *Page) SetDefaultTimeout(timeout int64) { p.logger.Debugf("Page:SetDefaultTimeout", "sid:%v timeout:%d", p.sessionID(), timeout) - p.timeoutSettings.setDefaultTimeout(timeout) + p.timeoutSettings.setDefaultTimeout(time.Duration(timeout) * time.Millisecond) } // SetExtraHTTPHeaders sets default HTTP headers for page and whole frame hierarchy. @@ -1017,6 +1128,87 @@ func (p *Page) Workers() []api.Worker { return workers } +func (p *Page) onConsoleAPICalled(event *cdpruntime.EventConsoleAPICalled) { + // If there are no handlers for EventConsoleAPICalled, return + p.eventHandlersMu.RLock() + if _, ok := p.eventHandlers[eventPageConsoleAPICalled]; !ok { + p.eventHandlersMu.RUnlock() + return + } + p.eventHandlersMu.RUnlock() + + m, err := p.consoleMsgFromConsoleEvent(event) + if err != nil { + p.logger.Errorf("Page:onConsoleAPICalled", "building console message: %v", err) + return + } + + p.eventHandlersMu.RLock() + defer p.eventHandlersMu.RUnlock() + for _, h := range p.eventHandlers[eventPageConsoleAPICalled] { + h := h + // Use TaskQueue in order to synchronize handlers execution in the event loop, + // as it is not thread safe and events are processed from a background goroutine + p.tq.Queue(func() error { + if err := h(m); err != nil { + return fmt.Errorf("executing onConsoleAPICalled handler: %w", err) + } + return nil + }) + } +} + +func (p *Page) consoleMsgFromConsoleEvent(e *cdpruntime.EventConsoleAPICalled) (*api.ConsoleMessage, error) { + execCtx, err := p.executionContextForID(e.ExecutionContextID) + if err != nil { + return nil, err + } + + var ( + l = p.logger.WithTime(e.Timestamp.Time()). + WithField("source", "browser"). + WithField("browser_source", "console-api") + + objects = make([]any, 0, len(e.Args)) + objectHandles = make([]api.JSHandle, 0, len(e.Args)) + ) + + for _, robj := range e.Args { + i, err := parseRemoteObject(robj) + if err != nil { + handleParseRemoteObjectErr(p.ctx, err, l) + } + + objects = append(objects, i) + objectHandles = append(objectHandles, NewJSHandle( + p.ctx, p.session, execCtx, execCtx.Frame(), robj, p.logger, + )) + } + + return &api.ConsoleMessage{ + Args: objectHandles, + Page: p, + Text: textForConsoleEvent(e, objects), + Type: e.Type.String(), + }, nil +} + +// executionContextForID returns the page ExecutionContext for the given ID. +func (p *Page) executionContextForID( + executionContextID cdpruntime.ExecutionContextID, +) (*ExecutionContext, error) { + p.frameSessionsMu.RLock() + defer p.frameSessionsMu.RUnlock() + + for _, fs := range p.frameSessions { + if exc, err := fs.executionContextForID(executionContextID); err == nil { + return exc, nil + } + } + + return nil, fmt.Errorf("no execution context found for id: %v", executionContextID) +} + // sessionID returns the Page's session ID. // It should be used internally in the Page. func (p *Page) sessionID() (sid target.SessionID) { @@ -1025,3 +1217,30 @@ func (p *Page) sessionID() (sid target.SessionID) { } return sid } + +// textForConsoleEvent generates the text representation for a consoleAPICalled event +// mimicking Playwright's behavior. +func textForConsoleEvent(e *cdpruntime.EventConsoleAPICalled, args []any) string { + if e.Type.String() == "dir" || e.Type.String() == "dirxml" || + e.Type.String() == "table" { + if len(e.Args) > 0 { + // These commands accept a single arg + return e.Args[0].Description + } + return "" + } + + // args is a mix of string and non strings, so using fmt.Sprint(args...) + // might not add spaces between all elements, therefore use a strings.Builder + // and handle format and concatenation + var b strings.Builder + for i, a := range args { + format := " %v" + if i == 0 { + format = "%v" + } + b.WriteString(fmt.Sprintf(format, a)) + } + + return b.String() +} diff --git a/vendor/github.com/grafana/xk6-browser/common/timeout.go b/vendor/github.com/grafana/xk6-browser/common/timeout.go index 68cfd6b4d27..883a5bbd1c8 100644 --- a/vendor/github.com/grafana/xk6-browser/common/timeout.go +++ b/vendor/github.com/grafana/xk6-browser/common/timeout.go @@ -1,10 +1,12 @@ package common +import "time" + // TimeoutSettings holds information on timeout settings. type TimeoutSettings struct { parent *TimeoutSettings - defaultTimeout *int64 - defaultNavigationTimeout *int64 + defaultTimeout *time.Duration + defaultNavigationTimeout *time.Duration } // NewTimeoutSettings creates a new timeout settings object. @@ -17,15 +19,15 @@ func NewTimeoutSettings(parent *TimeoutSettings) *TimeoutSettings { return t } -func (t *TimeoutSettings) setDefaultTimeout(timeout int64) { +func (t *TimeoutSettings) setDefaultTimeout(timeout time.Duration) { t.defaultTimeout = &timeout } -func (t *TimeoutSettings) setDefaultNavigationTimeout(timeout int64) { +func (t *TimeoutSettings) setDefaultNavigationTimeout(timeout time.Duration) { t.defaultNavigationTimeout = &timeout } -func (t *TimeoutSettings) navigationTimeout() int64 { +func (t *TimeoutSettings) navigationTimeout() time.Duration { if t.defaultNavigationTimeout != nil { return *t.defaultNavigationTimeout } @@ -35,15 +37,15 @@ func (t *TimeoutSettings) navigationTimeout() int64 { if t.parent != nil { return t.parent.navigationTimeout() } - return int64(DefaultTimeout.Seconds()) + return DefaultTimeout } -func (t *TimeoutSettings) timeout() int64 { +func (t *TimeoutSettings) timeout() time.Duration { if t.defaultTimeout != nil { return *t.defaultTimeout } if t.parent != nil { return t.parent.timeout() } - return int64(DefaultTimeout.Seconds()) + return DefaultTimeout } diff --git a/vendor/github.com/grafana/xk6-browser/env/env.go b/vendor/github.com/grafana/xk6-browser/env/env.go index 4029ca122e0..7c3ac7828cd 100644 --- a/vendor/github.com/grafana/xk6-browser/env/env.go +++ b/vendor/github.com/grafana/xk6-browser/env/env.go @@ -1,7 +1,10 @@ // Package env provides types to interact with environment setup. package env -import "os" +import ( + "os" + "strconv" +) // Execution specific. const ( @@ -66,7 +69,7 @@ const ( type LookupFunc func(key string) (string, bool) // EmptyLookup is a LookupFunc that always returns "" and false. -func EmptyLookup(key string) (string, bool) { return "", false } +func EmptyLookup(_ string) (string, bool) { return "", false } // Lookup is a LookupFunc that uses os.LookupEnv. func Lookup(key string) (string, bool) { return os.LookupEnv(key) } @@ -82,3 +85,26 @@ func ConstLookup(k, v string) LookupFunc { return EmptyLookup(key) } } + +// LookupBool returns the result of Lookup as a bool. +// If the key does not exist or the value is not a valid bool, it returns false. +// Otherwise it returns the bool value and true. +func LookupBool(key string) (value bool, ok bool) { + v, ok := Lookup(key) + if !ok { + return false, false + } + bv, err := strconv.ParseBool(v) + if err != nil { + return false, true + } + return bv, true +} + +// IsBrowserHeadless returns true if the BrowserHeadless environment +// variable is not set or set to true. +// The default behaviour is to run the browser in headless mode. +func IsBrowserHeadless() bool { + v, ok := LookupBool(BrowserHeadless) + return !ok || v +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 72c70bc159e..d7f408b8e59 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -148,7 +148,7 @@ github.com/google/uuid # github.com/gorilla/websocket v1.5.0 ## explicit; go 1.12 github.com/gorilla/websocket -# github.com/grafana/xk6-browser v1.0.2 +# github.com/grafana/xk6-browser v1.1.0 ## explicit; go 1.19 github.com/grafana/xk6-browser/api github.com/grafana/xk6-browser/browser