diff --git a/api/browser_type.go b/api/browser_type.go index 07f047c3d..e10afcfad 100644 --- a/api/browser_type.go +++ b/api/browser_type.go @@ -6,9 +6,9 @@ import ( // BrowserType is the public interface of a CDP browser client. type BrowserType interface { - Connect(wsEndpoint string, opts goja.Value) Browser + Connect(wsEndpoint string) Browser ExecutablePath() string - Launch(opts goja.Value) (_ Browser, browserProcessID int) + Launch() (_ Browser, browserProcessID int) LaunchPersistentContext(userDataDir string, opts goja.Value) Browser Name() string } diff --git a/browser/mapping.go b/browser/mapping.go index fe8012c6e..5b3cff318 100644 --- a/browser/mapping.go +++ b/browser/mapping.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/chromium" + "github.com/grafana/xk6-browser/env" "github.com/grafana/xk6-browser/k6error" "github.com/grafana/xk6-browser/k6ext" @@ -36,7 +37,7 @@ func mapBrowserToGoja(vu moduleVU) *goja.Object { obj = rt.NewObject() // TODO: Use k6 LookupEnv instead of OS package methods. // See https://github.com/grafana/xk6-browser/issues/822. - wsURL, isRemoteBrowser = k6ext.IsRemoteBrowser(os.LookupEnv) + wsURL, isRemoteBrowser = env.IsRemoteBrowser(os.LookupEnv) browserType = chromium.NewBrowserType(vu) ) for k, v := range mapBrowserType(vu, browserType, wsURL, isRemoteBrowser) { @@ -714,7 +715,7 @@ func mapBrowserType(vu moduleVU, bt api.BrowserType, wsURL string, isRemoteBrows rt := vu.Runtime() return mapping{ "connect": func(wsEndpoint string, opts goja.Value) *goja.Object { - b := bt.Connect(wsEndpoint, opts) + b := bt.Connect(wsEndpoint) m := mapBrowser(vu, b) return rt.ToValue(m).ToObject(rt) }, @@ -726,11 +727,11 @@ func mapBrowserType(vu moduleVU, bt api.BrowserType, wsURL string, isRemoteBrows // to connect and avoid storing the browser pid // as we have no access to it. if isRemoteBrowser { - m := mapBrowser(vu, bt.Connect(wsURL, opts)) + m := mapBrowser(vu, bt.Connect(wsURL)) return rt.ToValue(m).ToObject(rt) } - b, pid := bt.Launch(opts) + b, pid := bt.Launch() // store the pid so we can kill it later on panic. vu.registerPid(pid) m := mapBrowser(vu, b) diff --git a/chromium/browser_type.go b/chromium/browser_type.go index 5582e50dd..85acb9f06 100644 --- a/chromium/browser_type.go +++ b/chromium/browser_type.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/env" "github.com/grafana/xk6-browser/k6ext" "github.com/grafana/xk6-browser/log" "github.com/grafana/xk6-browser/storage" @@ -34,12 +35,13 @@ var _ api.BrowserType = &BrowserType{} type BrowserType struct { // FIXME: This is only exported because testBrowser needs it. Contexts // shouldn't be stored on structs if we can avoid it. - Ctx context.Context - vu k6modules.VU - hooks *common.Hooks - k6Metrics *k6ext.CustomMetrics - execPath string // path to the Chromium executable - randSrc *rand.Rand + Ctx context.Context + vu k6modules.VU + hooks *common.Hooks + k6Metrics *k6ext.CustomMetrics + execPath string // path to the Chromium executable + randSrc *rand.Rand + envLookupper env.LookupFunc } // NewBrowserType registers our custom k6 metrics, creates method mappings on @@ -49,18 +51,19 @@ func NewBrowserType(vu k6modules.VU) api.BrowserType { // otherwise it will return nil. k6m := k6ext.RegisterCustomMetrics(vu.InitEnv().Registry) b := BrowserType{ - vu: vu, - hooks: common.NewHooks(), - k6Metrics: k6m, - randSrc: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint: gosec + vu: vu, + hooks: common.NewHooks(), + k6Metrics: k6m, + randSrc: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint: gosec + envLookupper: os.LookupEnv, } return &b } func (b *BrowserType) init( - opts goja.Value, isRemoteBrowser bool, -) (context.Context, *common.LaunchOptions, *log.Logger, error) { + isRemoteBrowser bool, +) (context.Context, *common.BrowserOptions, *log.Logger, error) { ctx := b.initContext() logger, err := makeLogger(ctx) @@ -68,26 +71,27 @@ func (b *BrowserType) init( return nil, nil, nil, fmt.Errorf("error setting up logger: %w", err) } - var launchOpts *common.LaunchOptions + var browserOpts *common.BrowserOptions if isRemoteBrowser { - launchOpts = common.NewRemoteBrowserLaunchOptions() + browserOpts = common.NewRemoteBrowserOptions() } else { - launchOpts = common.NewLaunchOptions() + browserOpts = common.NewLocalBrowserOptions() } - if err = launchOpts.Parse(ctx, logger, opts); err != nil { - return nil, nil, nil, fmt.Errorf("error parsing launch options: %w", err) + opts := k6ext.GetScenarioOpts(b.vu.Context(), b.vu) + if err = browserOpts.Parse(ctx, logger, opts, b.envLookupper); err != nil { + return nil, nil, nil, fmt.Errorf("error parsing browser options: %w", err) } - ctx = common.WithLaunchOptions(ctx, launchOpts) + ctx = common.WithBrowserOptions(ctx, browserOpts) - if err := logger.SetCategoryFilter(launchOpts.LogCategoryFilter); err != nil { + if err := logger.SetCategoryFilter(browserOpts.LogCategoryFilter); err != nil { return nil, nil, nil, fmt.Errorf("error setting category filter: %w", err) } - if launchOpts.Debug { + if browserOpts.Debug { _ = logger.SetLevel("debug") } - return ctx, launchOpts, logger, nil + return ctx, browserOpts, logger, nil } func (b *BrowserType) initContext() context.Context { @@ -99,17 +103,17 @@ func (b *BrowserType) initContext() context.Context { } // 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) +func (b *BrowserType) Connect(wsEndpoint string) api.Browser { + ctx, browserOpts, logger, err := b.init(true) if err != nil { k6ext.Panic(ctx, "initializing browser type: %w", err) } - bp, err := b.connect(ctx, wsEndpoint, launchOpts, logger) + bp, err := b.connect(ctx, wsEndpoint, browserOpts, logger) if err != nil { err = &k6ext.UserFriendlyError{ Err: err, - Timeout: launchOpts.Timeout, + Timeout: browserOpts.Timeout, } k6ext.Panic(ctx, "%w", err) } @@ -118,7 +122,7 @@ func (b *BrowserType) Connect(wsEndpoint string, opts goja.Value) api.Browser { } func (b *BrowserType) connect( - ctx context.Context, wsURL string, opts *common.LaunchOptions, logger *log.Logger, + ctx context.Context, wsURL string, opts *common.BrowserOptions, logger *log.Logger, ) (*common.Browser, error) { browserProc, err := b.link(ctx, wsURL, logger) if browserProc == nil { @@ -154,17 +158,17 @@ func (b *BrowserType) link( // 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) +func (b *BrowserType) Launch() (_ api.Browser, browserProcessID int) { + ctx, browserOpts, logger, err := b.init(false) if err != nil { k6ext.Panic(ctx, "initializing browser type: %w", err) } - bp, pid, err := b.launch(ctx, launchOpts, logger) + bp, pid, err := b.launch(ctx, browserOpts, logger) if err != nil { err = &k6ext.UserFriendlyError{ Err: err, - Timeout: launchOpts.Timeout, + Timeout: browserOpts.Timeout, } k6ext.Panic(ctx, "%w", err) } @@ -173,7 +177,7 @@ func (b *BrowserType) Launch(opts goja.Value) (_ api.Browser, browserProcessID i } func (b *BrowserType) launch( - ctx context.Context, opts *common.LaunchOptions, logger *log.Logger, + ctx context.Context, opts *common.BrowserOptions, logger *log.Logger, ) (_ *common.Browser, pid int, _ error) { flags, err := prepareFlags(opts, &(b.vu.State()).Options) if err != nil { @@ -231,7 +235,7 @@ func (b *BrowserType) Name() string { // allocate starts a new Chromium browser process and returns it. func (b *BrowserType) allocate( - ctx context.Context, opts *common.LaunchOptions, + ctx context.Context, opts *common.BrowserOptions, flags map[string]any, dataDir *storage.Dir, logger *log.Logger, ) (_ *common.BrowserProcess, rerr error) { @@ -295,6 +299,11 @@ func (b *BrowserType) ExecutablePath() (execPath string) { return "" } +// SetEnvLookupper sets the environment variable lookupper function. +func (b *BrowserType) SetEnvLookupper(envLookupper env.LookupFunc) { + b.envLookupper = envLookupper +} + // parseArgs parses command-line arguments and returns them. func parseArgs(flags map[string]any) ([]string, error) { // Build command line args list @@ -322,7 +331,7 @@ func parseArgs(flags map[string]any) ([]string, error) { return args, nil } -func prepareFlags(lopts *common.LaunchOptions, k6opts *k6lib.Options) (map[string]any, error) { +func prepareFlags(lopts *common.BrowserOptions, k6opts *k6lib.Options) (map[string]any, error) { // After Puppeteer's and Playwright's default behavior. f := map[string]any{ "disable-background-networking": true, diff --git a/chromium/browser_type_test.go b/chromium/browser_type_test.go index 296790e03..47cf31a4c 100644 --- a/chromium/browser_type_test.go +++ b/chromium/browser_type_test.go @@ -28,36 +28,36 @@ func TestBrowserTypePrepareFlags(t *testing.T) { testCases := []struct { flag string - changeOpts *common.LaunchOptions + changeOpts *common.BrowserOptions changeK6Opts *k6lib.Options expInitVal, expChangedVal any post func(t *testing.T, flags map[string]any) }{ { flag: "hide-scrollbars", - changeOpts: &common.LaunchOptions{IgnoreDefaultArgs: []string{"hide-scrollbars"}, Headless: true}, + changeOpts: &common.BrowserOptions{IgnoreDefaultArgs: []string{"hide-scrollbars"}, Headless: true}, }, { flag: "hide-scrollbars", - changeOpts: &common.LaunchOptions{Headless: true}, + changeOpts: &common.BrowserOptions{Headless: true}, expChangedVal: true, }, { flag: "browser-arg", expInitVal: nil, - changeOpts: &common.LaunchOptions{Args: []string{"browser-arg=value"}}, + changeOpts: &common.BrowserOptions{Args: []string{"browser-arg=value"}}, expChangedVal: "value", }, { flag: "browser-arg-flag", expInitVal: nil, - changeOpts: &common.LaunchOptions{Args: []string{"browser-arg-flag"}}, + changeOpts: &common.BrowserOptions{Args: []string{"browser-arg-flag"}}, expChangedVal: "", }, { flag: "browser-arg-trim-double-quote", expInitVal: nil, - changeOpts: &common.LaunchOptions{Args: []string{ + changeOpts: &common.BrowserOptions{Args: []string{ ` browser-arg-trim-double-quote = "value " `, }}, expChangedVal: "value ", @@ -65,7 +65,7 @@ func TestBrowserTypePrepareFlags(t *testing.T) { { flag: "browser-arg-trim-single-quote", expInitVal: nil, - changeOpts: &common.LaunchOptions{Args: []string{ + changeOpts: &common.BrowserOptions{Args: []string{ ` browser-arg-trim-single-quote=' value '`, }}, expChangedVal: " value ", @@ -73,7 +73,7 @@ func TestBrowserTypePrepareFlags(t *testing.T) { { flag: "browser-args", expInitVal: nil, - changeOpts: &common.LaunchOptions{Args: []string{ + changeOpts: &common.BrowserOptions{Args: []string{ "browser-arg1='value1", "browser-arg2=''value2''", "browser-flag", }}, post: func(t *testing.T, flags map[string]any) { @@ -87,7 +87,7 @@ func TestBrowserTypePrepareFlags(t *testing.T) { { flag: "host-resolver-rules", expInitVal: nil, - changeOpts: &common.LaunchOptions{Args: []string{ + changeOpts: &common.BrowserOptions{Args: []string{ `host-resolver-rules="MAP * www.example.com, EXCLUDE *.youtube.*"`, }}, changeK6Opts: &k6lib.Options{ @@ -99,14 +99,14 @@ func TestBrowserTypePrepareFlags(t *testing.T) { { flag: "host-resolver-rules", expInitVal: nil, - changeOpts: &common.LaunchOptions{}, + changeOpts: &common.BrowserOptions{}, changeK6Opts: &k6lib.Options{}, expChangedVal: nil, }, { flag: "headless", expInitVal: false, - changeOpts: &common.LaunchOptions{Headless: true}, + changeOpts: &common.BrowserOptions{Headless: true}, expChangedVal: true, post: func(t *testing.T, flags map[string]any) { t.Helper() @@ -124,7 +124,7 @@ func TestBrowserTypePrepareFlags(t *testing.T) { t.Run(tc.flag, func(t *testing.T) { t.Parallel() - flags, err := prepareFlags(&common.LaunchOptions{}, nil) + flags, err := prepareFlags(&common.BrowserOptions{}, nil) require.NoError(t, err, "failed to prepare flags") if tc.expInitVal != nil { diff --git a/common/browser.go b/common/browser.go index 3c1d53d7a..f1b7348ba 100644 --- a/common/browser.go +++ b/common/browser.go @@ -44,7 +44,7 @@ type Browser struct { state int64 browserProc *BrowserProcess - launchOpts *LaunchOptions + browserOpts *BrowserOptions // Connection to the browser to talk CDP protocol. // A *Connection is saved to this field, see: connect(). @@ -78,10 +78,10 @@ func NewBrowser( ctx context.Context, cancel context.CancelFunc, browserProc *BrowserProcess, - launchOpts *LaunchOptions, + browserOpts *BrowserOptions, logger *log.Logger, ) (*Browser, error) { - b := newBrowser(ctx, cancel, browserProc, launchOpts, logger) + b := newBrowser(ctx, cancel, browserProc, browserOpts, logger) if err := b.connect(); err != nil { return nil, err } @@ -93,7 +93,7 @@ func newBrowser( ctx context.Context, cancelFn context.CancelFunc, browserProc *BrowserProcess, - launchOpts *LaunchOptions, + browserOpts *BrowserOptions, logger *log.Logger, ) *Browser { return &Browser{ @@ -102,7 +102,7 @@ func newBrowser( cancelFn: cancelFn, state: int64(BrowserStateOpen), browserProc: browserProc, - launchOpts: launchOpts, + browserOpts: browserOpts, contexts: make(map[cdp.BrowserContextID]*BrowserContext), pages: make(map[target.ID]*Page), sessionIDtoTargetID: make(map[target.SessionID]target.ID), @@ -384,7 +384,7 @@ func (b *Browser) newPageInContext(id cdp.BrowserContextID) (*Page, error) { return nil, fmt.Errorf("missing browser context: %s", id) } - ctx, cancel := context.WithTimeout(b.ctx, b.launchOpts.Timeout) + ctx, cancel := context.WithTimeout(b.ctx, b.browserOpts.Timeout) defer cancel() // buffer of one is for sending the target ID whether an event handler @@ -425,7 +425,7 @@ func (b *Browser) newPageInContext(id cdp.BrowserContextID) (*Page, error) { case <-ctx.Done(): err = &k6ext.UserFriendlyError{ Err: ctx.Err(), - Timeout: b.launchOpts.Timeout, + Timeout: b.browserOpts.Timeout, } b.logger.Debugf("Browser:newPageInContext:<-ctx.Done", "tid:%v bctxid:%v err:%v", tid, id, err) } @@ -462,7 +462,7 @@ func (b *Browser) Close() { // 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 { + if !b.browserOpts.isRemoteBrowser { var closeErr *websocket.CloseError err := cdpbrowser.Close().Do(cdp.WithExecutor(b.ctx, b.conn)) if err != nil && !errors.As(err, &closeErr) { diff --git a/common/browser_context.go b/common/browser_context.go index b98e8a6da..82a4540b8 100644 --- a/common/browser_context.go +++ b/common/browser_context.go @@ -368,7 +368,7 @@ func (b *BrowserContext) WaitForEvent(event string, optsOrPredicate goja.Value) isCallable bool predicateFn goja.Callable // TODO: Find out whether * time.Second is necessary. - timeout = b.browser.launchOpts.Timeout * time.Second //nolint:durationcheck + timeout = b.browser.browserOpts.Timeout * time.Second //nolint:durationcheck ) if gojaValueExists(optsOrPredicate) { switch optsOrPredicate.ExportType() { diff --git a/common/browser_context_test.go b/common/browser_context_test.go index 6cf1386af..be52dccd9 100644 --- a/common/browser_context_test.go +++ b/common/browser_context_test.go @@ -17,7 +17,7 @@ func TestNewBrowserContext(t *testing.T) { t.Run("add_web_vital_js_scripts_to_context", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) logger := log.NewNullLogger() - b := newBrowser(ctx, cancel, nil, NewLaunchOptions(), logger) + b := newBrowser(ctx, cancel, nil, NewLocalBrowserOptions(), logger) vu := k6test.NewVU(t) ctx = k6ext.WithVU(ctx, vu) diff --git a/common/browser_options.go b/common/browser_options.go index f1c71fecf..3f45fda1d 100644 --- a/common/browser_options.go +++ b/common/browser_options.go @@ -2,58 +2,64 @@ package common import ( "context" + "errors" + "fmt" + "strconv" + "strings" "time" - "github.com/dop251/goja" - - "github.com/grafana/xk6-browser/k6ext" + "github.com/grafana/xk6-browser/env" "github.com/grafana/xk6-browser/log" + + "go.k6.io/k6/lib/types" ) const ( - optArgs = "args" - optDebug = "debug" - optExecutablePath = "executablePath" - optHeadless = "headless" - optIgnoreDefaultArgs = "ignoreDefaultArgs" - optLogCategoryFilter = "logCategoryFilter" - optSlowMo = "slowMo" - optTimeout = "timeout" + // Script variables. + + optType = "type" + + // ENV variables. + + optArgs = "K6_BROWSER_ARGS" + optDebug = "K6_BROWSER_DEBUG" + optExecutablePath = "K6_BROWSER_EXECUTABLE_PATH" + optHeadless = "K6_BROWSER_HEADLESS" + optIgnoreDefaultArgs = "K6_BROWSER_IGNORE_DEFAULT_ARGS" + optLogCategoryFilter = "K6_BROWSER_LOG_CATEGORY_FILTER" + optTimeout = "K6_BROWSER_TIMEOUT" ) -// LaunchOptions stores browser launch options. -type LaunchOptions struct { +// BrowserOptions stores browser options. +type BrowserOptions struct { Args []string Debug bool ExecutablePath string Headless bool IgnoreDefaultArgs []string LogCategoryFilter string - SlowMo time.Duration - Timeout time.Duration + // TODO: Do not expose slowMo option by now. + // See https://github.com/grafana/xk6-browser/issues/857. + SlowMo time.Duration + Timeout time.Duration isRemoteBrowser bool // some options will be ignored if browser is in a remote machine } -// LaunchPersistentContextOptions stores browser launch options for persistent context. -type LaunchPersistentContextOptions struct { - LaunchOptions - BrowserContextOptions -} - -// NewLaunchOptions returns a new LaunchOptions. -func NewLaunchOptions() *LaunchOptions { - return &LaunchOptions{ +// NewLocalBrowserOptions returns a new BrowserOptions +// for a browser launched in the local machine. +func NewLocalBrowserOptions() *BrowserOptions { + return &BrowserOptions{ Headless: true, LogCategoryFilter: ".*", Timeout: DefaultTimeout, } } -// NewRemoteBrowserLaunchOptions returns a new LaunchOptions +// NewRemoteBrowserOptions returns a new BrowserOptions // for a browser running in a remote machine. -func NewRemoteBrowserLaunchOptions() *LaunchOptions { - return &LaunchOptions{ +func NewRemoteBrowserOptions() *BrowserOptions { + return &BrowserOptions{ Headless: true, LogCategoryFilter: ".*", Timeout: DefaultTimeout, @@ -61,51 +67,58 @@ func NewRemoteBrowserLaunchOptions() *LaunchOptions { } } -// Parse parses launch options from a JS object. -func (l *LaunchOptions) Parse(ctx context.Context, logger *log.Logger, opts goja.Value) error { //nolint:cyclop - // when opts is nil, we just return the default options without error. - if !gojaValueExists(opts) { - return nil +// Parse parses browser options from a JS object. +func (bo *BrowserOptions) Parse( //nolint:cyclop + ctx context.Context, logger *log.Logger, opts map[string]any, envLookup env.LookupFunc, +) error { + // Parse opts + bt, ok := opts[optType] + // Only 'chromium' is supported by now, so return error + // if type option is not set, or if it's set and its value + // is different than 'chromium' + if !ok { + return errors.New("browser type option must be set") } - var ( - rt = k6ext.Runtime(ctx) - o = opts.ToObject(rt) - defaults = map[string]any{ - optHeadless: l.Headless, - optLogCategoryFilter: l.LogCategoryFilter, - optTimeout: l.Timeout, - } - ) - for _, k := range o.Keys() { - if l.shouldIgnoreIfBrowserIsRemote(k) { - logger.Warnf("LaunchOptions", "setting %s option is disallowed when browser is remote", k) + if bt != "chromium" { + return fmt.Errorf("unsupported browser type: %s", bt) + } + + // Parse env + envOpts := [...]string{ + optArgs, + optDebug, + optExecutablePath, + optHeadless, + optIgnoreDefaultArgs, + optLogCategoryFilter, + optTimeout, + } + + for _, e := range envOpts { + ev, ok := envLookup(e) + if !ok || ev == "" { continue } - v := o.Get(k) - if v.Export() == nil { - if dv, ok := defaults[k]; ok { - logger.Warnf("LaunchOptions", "%s was null and set to its default: %v", k, dv) - } + if bo.shouldIgnoreIfBrowserIsRemote(e) { + logger.Warnf("BrowserOptions", "setting %s option is disallowed when browser is remote", e) continue } var err error - switch k { + switch e { case optArgs: - err = exportOpt(rt, k, v, &l.Args) + bo.Args = parseListOpt(ev) case optDebug: - l.Debug, err = parseBoolOpt(k, v) + bo.Debug, err = parseBoolOpt(e, ev) case optExecutablePath: - l.ExecutablePath, err = parseStrOpt(k, v) + bo.ExecutablePath = ev case optHeadless: - l.Headless, err = parseBoolOpt(k, v) + bo.Headless, err = parseBoolOpt(e, ev) case optIgnoreDefaultArgs: - err = exportOpt(rt, k, v, &l.IgnoreDefaultArgs) + bo.IgnoreDefaultArgs = parseListOpt(ev) case optLogCategoryFilter: - l.LogCategoryFilter, err = parseStrOpt(k, v) - case optSlowMo: - l.SlowMo, err = parseTimeOpt(k, v) + bo.LogCategoryFilter = ev case optTimeout: - l.Timeout, err = parseTimeOpt(k, v) + bo.Timeout, err = parseTimeOpt(e, ev) } if err != nil { return err @@ -115,8 +128,8 @@ func (l *LaunchOptions) Parse(ctx context.Context, logger *log.Logger, opts goja return nil } -func (l *LaunchOptions) shouldIgnoreIfBrowserIsRemote(opt string) bool { - if !l.isRemoteBrowser { +func (bo *BrowserOptions) shouldIgnoreIfBrowserIsRemote(opt string) bool { + if !bo.isRemoteBrowser { return false } @@ -130,3 +143,33 @@ func (l *LaunchOptions) shouldIgnoreIfBrowserIsRemote(opt string) bool { return ignore } + +func parseBoolOpt(k, v string) (bool, error) { + b, err := strconv.ParseBool(v) + if err != nil { + return false, fmt.Errorf("%s should be a boolean", k) + } + + return b, nil +} + +func parseTimeOpt(k, v string) (time.Duration, error) { + t, err := types.GetDurationValue(v) + if err != nil { + return time.Duration(0), fmt.Errorf("%s should be a time duration value: %w", k, err) + } + + return t, nil +} + +func parseListOpt(v string) []string { + elems := strings.Split(v, ",") + // If last element is a void string, + // because value contained an ending comma, + // remove it + if elems[len(elems)-1] == "" { + elems = elems[:len(elems)-1] + } + + return elems +} diff --git a/common/browser_options_test.go b/common/browser_options_test.go index ffac6dcab..951983916 100644 --- a/common/browser_options_test.go +++ b/common/browser_options_test.go @@ -7,62 +7,86 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/xk6-browser/env" "github.com/grafana/xk6-browser/k6ext/k6test" "github.com/grafana/xk6-browser/log" ) -func TestBrowserLaunchOptionsParse(t *testing.T) { +func TestBrowserOptionsParse(t *testing.T) { //nolint:gocognit t.Parallel() - defaultOptions := &LaunchOptions{ + defaultOptions := &BrowserOptions{ Headless: true, LogCategoryFilter: ".*", Timeout: DefaultTimeout, } + noopEnvLookuper := func(string) (string, bool) { + return "", false + } + for name, tt := range map[string]struct { opts map[string]any - assert func(testing.TB, *LaunchOptions) + envLookupper env.LookupFunc + assert func(testing.TB, *BrowserOptions) err string isRemoteBrowser bool }{ "defaults": { - opts: map[string]any{}, - assert: func(tb testing.TB, lo *LaunchOptions) { + opts: map[string]any{ + "type": "chromium", + }, + envLookupper: noopEnvLookuper, + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() assert.Equal(t, defaultOptions, lo) }, }, "defaults_nil": { // providing nil option returns default options - opts: nil, - assert: func(tb testing.TB, lo *LaunchOptions) { + opts: map[string]any{ + "type": "chromium", + }, + envLookupper: noopEnvLookuper, + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() assert.Equal(t, defaultOptions, lo) }, }, "defaults_remote_browser": { - isRemoteBrowser: true, opts: map[string]any{ + "type": "chromium", + }, + isRemoteBrowser: true, + envLookupper: func(k string) (string, bool) { + switch k { // disallow changing the following opts - "args": []string{"any"}, - "executablePath": "something else", - "headless": false, - "ignoreDefaultArgs": []string{"any"}, + case optArgs: + return "any", true + case optExecutablePath: + return "something else", true + case optHeadless: + return "false", true + case optIgnoreDefaultArgs: + return "any", true // allow changing the following opts - "debug": true, - "logCategoryFilter": "...", - "slowMo": time.Second, - "timeout": time.Second, + case optDebug: + return "true", true + case optLogCategoryFilter: + return "...", true + case optTimeout: + return "1s", true + default: + return "", false + } }, - assert: func(tb testing.TB, lo *LaunchOptions) { + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() - assert.Equal(t, &LaunchOptions{ + assert.Equal(t, &BrowserOptions{ // disallowed: Headless: true, // allowed: Debug: true, LogCategoryFilter: "...", - SlowMo: time.Second, Timeout: time.Second, isRemoteBrowser: true, @@ -71,13 +95,14 @@ func TestBrowserLaunchOptionsParse(t *testing.T) { }, "nulls": { // don't override the defaults on `null` opts: map[string]any{ - "headless": nil, - "logCategoryFilter": nil, - "timeout": nil, + "type": "chromium", }, - assert: func(tb testing.TB, lo *LaunchOptions) { + envLookupper: func(k string) (string, bool) { + return "", true + }, + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() - assert.Equal(tb, &LaunchOptions{ + assert.Equal(tb, &BrowserOptions{ Headless: true, LogCategoryFilter: ".*", Timeout: DefaultTimeout, @@ -86,9 +111,15 @@ func TestBrowserLaunchOptionsParse(t *testing.T) { }, "args": { opts: map[string]any{ - "args": []any{"browser-arg1='value1", "browser-arg2=value2", "browser-flag"}, + "type": "chromium", + }, + envLookupper: func(k string) (string, bool) { + if k == optArgs { + return "browser-arg1='value1,browser-arg2=value2,browser-flag", true + } + return "", false }, - assert: func(tb testing.TB, lo *LaunchOptions) { + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() require.Len(tb, lo.Args, 3) assert.Equal(tb, "browser-arg1='value1", lo.Args[0]) @@ -96,102 +127,154 @@ func TestBrowserLaunchOptionsParse(t *testing.T) { assert.Equal(tb, "browser-flag", lo.Args[2]) }, }, - "args_err": { + "debug": { opts: map[string]any{ - "args": 1, + "type": "chromium", }, - err: "args should be an array of strings", - }, - "debug": { - opts: map[string]any{"debug": true}, - assert: func(tb testing.TB, lo *LaunchOptions) { + envLookupper: func(k string) (string, bool) { + if k == optDebug { + return "true", true + } + return "", false + }, + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() assert.True(t, lo.Debug) }, }, "debug_err": { - opts: map[string]any{"debug": "true"}, - err: "debug should be a boolean", + opts: map[string]any{ + "type": "chromium", + }, + envLookupper: func(k string) (string, bool) { + if k == optDebug { + return "non-boolean", true + } + return "", false + }, + err: "K6_BROWSER_DEBUG should be a boolean", }, "executablePath": { opts: map[string]any{ - "executablePath": "cmd/somewhere", + "type": "chromium", + }, + envLookupper: func(k string) (string, bool) { + if k == optExecutablePath { + return "cmd/somewhere", true + } + return "", false }, - assert: func(tb testing.TB, lo *LaunchOptions) { + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() assert.Equal(t, "cmd/somewhere", lo.ExecutablePath) }, }, - "executablePath_err": { - opts: map[string]any{"executablePath": 1}, - err: "executablePath should be a string", - }, "headless": { opts: map[string]any{ - "headless": false, + "type": "chromium", + }, + envLookupper: func(k string) (string, bool) { + if k == optHeadless { + return "false", true + } + return "", false }, - assert: func(tb testing.TB, lo *LaunchOptions) { + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() assert.False(t, lo.Headless) }, }, "headless_err": { - opts: map[string]any{"headless": "true"}, - err: "headless should be a boolean", + opts: map[string]any{ + "type": "chromium", + }, + envLookupper: func(k string) (string, bool) { + if k == optHeadless { + return "non-boolean", true + } + return "", false + }, + err: "K6_BROWSER_HEADLESS should be a boolean", }, "ignoreDefaultArgs": { opts: map[string]any{ - "ignoreDefaultArgs": []string{"--hide-scrollbars", "--hide-something"}, + "type": "chromium", }, - assert: func(tb testing.TB, lo *LaunchOptions) { + envLookupper: func(k string) (string, bool) { + if k == optIgnoreDefaultArgs { + return "--hide-scrollbars,--hide-something", true + } + return "", false + }, + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() assert.Len(t, lo.IgnoreDefaultArgs, 2) assert.Equal(t, "--hide-scrollbars", lo.IgnoreDefaultArgs[0]) assert.Equal(t, "--hide-something", lo.IgnoreDefaultArgs[1]) }, }, - "ignoreDefaultArgs_err": { - opts: map[string]any{"ignoreDefaultArgs": "ABC"}, - err: "ignoreDefaultArgs should be an array of strings", - }, "logCategoryFilter": { opts: map[string]any{ - "logCategoryFilter": "**", + "type": "chromium", }, - assert: func(tb testing.TB, lo *LaunchOptions) { + envLookupper: func(k string) (string, bool) { + if k == optLogCategoryFilter { + return "**", true + } + return "", false + }, + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() assert.Equal(t, "**", lo.LogCategoryFilter) }, }, - "logCategoryFilter_err": { - opts: map[string]any{"logCategoryFilter": 1}, - err: "logCategoryFilter should be a string", - }, - "slowMo": { + "timeout": { opts: map[string]any{ - "slowMo": "5s", + "type": "chromium", }, - assert: func(tb testing.TB, lo *LaunchOptions) { + envLookupper: func(k string) (string, bool) { + if k == optTimeout { + return "10s", true + } + return "", false + }, + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() - assert.Equal(t, 5*time.Second, lo.SlowMo) + assert.Equal(t, 10*time.Second, lo.Timeout) }, }, - "slowMo_err": { - opts: map[string]any{"slowMo": "ABC"}, - err: "slowMo should be a time duration value", + "timeout_err": { + opts: map[string]any{ + "type": "chromium", + }, + envLookupper: func(k string) (string, bool) { + if k == optTimeout { + return "ABC", true + } + return "", false + }, + err: "K6_BROWSER_TIMEOUT should be a time duration value", }, - "timeout": { + "browser_type": { opts: map[string]any{ - "timeout": "10s", + "type": "chromium", }, - assert: func(tb testing.TB, lo *LaunchOptions) { + envLookupper: noopEnvLookuper, + assert: func(tb testing.TB, lo *BrowserOptions) { tb.Helper() - assert.Equal(t, 10*time.Second, lo.Timeout) + // Noop, just expect no error }, }, - "timeout_err": { - opts: map[string]any{"timeout": "ABC"}, - err: "timeout should be a time duration value", + "browser_type_err": { + opts: map[string]any{ + "type": "mybrowsertype", + }, + envLookupper: noopEnvLookuper, + err: "unsupported browser type: mybrowsertype", + }, + "browser_type_unset_err": { + envLookupper: noopEnvLookuper, + err: "browser type option must be set", }, } { tt := tt @@ -199,16 +282,16 @@ func TestBrowserLaunchOptionsParse(t *testing.T) { t.Parallel() var ( vu = k6test.NewVU(t) - lo *LaunchOptions + lo *BrowserOptions ) if tt.isRemoteBrowser { - lo = NewRemoteBrowserLaunchOptions() + lo = NewRemoteBrowserOptions() } else { - lo = NewLaunchOptions() + lo = NewLocalBrowserOptions() } - err := lo.Parse(vu.Context(), log.NewNullLogger(), vu.ToGojaValue(tt.opts)) + err := lo.Parse(vu.Context(), log.NewNullLogger(), tt.opts, tt.envLookupper) if tt.err != "" { require.ErrorContains(t, err, tt.err) } else { diff --git a/common/browser_test.go b/common/browser_test.go index 3a54803d1..c01539ade 100644 --- a/common/browser_test.go +++ b/common/browser_test.go @@ -26,7 +26,7 @@ func TestBrowserNewPageInContext(t *testing.T) { newTestCase := func(id cdp.BrowserContextID) *testCase { ctx, cancel := context.WithCancel(context.Background()) logger := log.NewNullLogger() - b := newBrowser(ctx, cancel, nil, NewLaunchOptions(), logger) + b := newBrowser(ctx, cancel, nil, NewLocalBrowserOptions(), logger) // set a new browser context in the browser with `id`, so that newPageInContext can find it. var err error vu := k6test.NewVU(t) @@ -129,7 +129,7 @@ func TestBrowserNewPageInContext(t *testing.T) { // set a lower timeout for catching the timeout error. const timeout = 100 * time.Millisecond // set the timeout for the browser value. - tc.b.launchOpts.Timeout = timeout + tc.b.browserOpts.Timeout = timeout tc.b.conn = fakeConn{ execute: func(context.Context, string, easyjson.Marshaler, easyjson.Unmarshaler) error { // executor takes more time than the timeout. diff --git a/common/context.go b/common/context.go index 8efe8689c..56f8d643d 100644 --- a/common/context.go +++ b/common/context.go @@ -7,7 +7,7 @@ import ( type ctxKey int const ( - ctxKeyLaunchOptions ctxKey = iota + ctxKeyBrowserOptions ctxKey = iota ctxKeyHooks ctxKeyIterationID ) @@ -35,16 +35,21 @@ func GetIterationID(ctx context.Context) string { return s } -func WithLaunchOptions(ctx context.Context, opts *LaunchOptions) context.Context { - return context.WithValue(ctx, ctxKeyLaunchOptions, opts) +// WithBrowserOptions adds the browser options to the context. +func WithBrowserOptions(ctx context.Context, opts *BrowserOptions) context.Context { + return context.WithValue(ctx, ctxKeyBrowserOptions, opts) } -func GetLaunchOptions(ctx context.Context) *LaunchOptions { - v := ctx.Value(ctxKeyLaunchOptions) +// GetBrowserOptions returns the browser options attached to the context. +func GetBrowserOptions(ctx context.Context) *BrowserOptions { + v := ctx.Value(ctxKeyBrowserOptions) if v == nil { return nil } - return v.(*LaunchOptions) + if bo, ok := v.(*BrowserOptions); ok { + return bo + } + return nil } // contextWithDoneChan returns a new context that is canceled either diff --git a/common/doc.go b/common/doc.go new file mode 100644 index 000000000..ed51123e2 --- /dev/null +++ b/common/doc.go @@ -0,0 +1,3 @@ +// Package common contains the implementation of API elements that do not +// depend on the browser type. +package common diff --git a/common/frame_session.go b/common/frame_session.go index 969b56ab6..9932ee22c 100644 --- a/common/frame_session.go +++ b/common/frame_session.go @@ -1076,7 +1076,7 @@ func (fs *FrameSession) updateViewport() error { // add an inset to viewport depending on the operating system. // this won't add an inset if we're running in headless mode. viewport.calculateInset( - fs.page.browserCtx.browser.launchOpts.Headless, + fs.page.browserCtx.browser.browserOpts.Headless, runtime.GOOS, ) action2 := browser.SetWindowBounds(fs.windowID, &browser.Bounds{ diff --git a/common/hooks.go b/common/hooks.go index abfa22005..dfa2340c2 100644 --- a/common/hooks.go +++ b/common/hooks.go @@ -30,7 +30,7 @@ func applySlowMo(ctx context.Context) { } func defaultSlowMo(ctx context.Context) { - sm := GetLaunchOptions(ctx).SlowMo + sm := GetBrowserOptions(ctx).SlowMo if sm <= 0 { return } diff --git a/common/options.go b/common/options.go deleted file mode 100644 index 9d4e5399c..000000000 --- a/common/options.go +++ /dev/null @@ -1,60 +0,0 @@ -package common - -import ( - "fmt" - "reflect" - "time" - - "github.com/dop251/goja" - - "go.k6.io/k6/lib/types" -) - -func parseBoolOpt(key string, val goja.Value) (b bool, err error) { - if val.ExportType().Kind() != reflect.Bool { - return false, fmt.Errorf("%s should be a boolean", key) - } - b, _ = val.Export().(bool) - return b, nil -} - -func parseStrOpt(key string, val goja.Value) (s string, err error) { - if val.ExportType().Kind() != reflect.String { - return "", fmt.Errorf("%s should be a string", key) - } - return val.String(), nil -} - -func parseTimeOpt(key string, val goja.Value) (t time.Duration, err error) { - if t, err = types.GetDurationValue(val.String()); err != nil { - return time.Duration(0), fmt.Errorf("%s should be a time duration value: %w", key, err) - } - return -} - -// exportOpt exports src to dst and dynamically returns an error -// depending on the type if an error occurs. Panics if dst is not -// a pointer and not points to a map, struct, or slice. -func exportOpt[T any](rt *goja.Runtime, key string, src goja.Value, dst T) error { - typ := reflect.TypeOf(dst) - if typ.Kind() != reflect.Pointer { - panic("dst should be a pointer") - } - kind := typ.Elem().Kind() - s, ok := map[reflect.Kind]string{ - reflect.Map: "a map", - reflect.Struct: "an object", - reflect.Slice: "an array of", - }[kind] - if !ok { - panic("dst should be one of: map, struct, slice") - } - if err := rt.ExportTo(src, dst); err != nil { - if kind == reflect.Slice { - s += fmt.Sprintf(" %ss", typ.Elem().Elem()) - } - return fmt.Errorf("%s should be %s: %w", key, s, err) - } - - return nil -} diff --git a/env/doc.go b/env/doc.go new file mode 100644 index 000000000..4f2054bb0 --- /dev/null +++ b/env/doc.go @@ -0,0 +1,2 @@ +// Package env provides methods to interact with environment setup. +package env diff --git a/k6ext/env.go b/env/env.go similarity index 82% rename from k6ext/env.go rename to env/env.go index cf0fd468b..7a134ba59 100644 --- a/k6ext/env.go +++ b/env/env.go @@ -1,4 +1,4 @@ -package k6ext +package env import ( "crypto/rand" @@ -6,7 +6,8 @@ import ( "strings" ) -type envLookupper func(key string) (string, bool) +// LookupFunc defines a function to look up a key from the environment. +type LookupFunc func(key string) (string, bool) // IsRemoteBrowser returns true and the corresponding CDP // WS URL if this one is set through the K6_BROWSER_WS_URL @@ -15,7 +16,7 @@ type envLookupper func(key string) (string, bool) // URLs, this method returns a randomly chosen URL from the list // so connections are done in a round-robin fashion for all the // entries in the list. -func IsRemoteBrowser(envLookup envLookupper) (wsURL string, isRemote bool) { +func IsRemoteBrowser(envLookup LookupFunc) (wsURL string, isRemote bool) { wsURL, isRemote = envLookup("K6_BROWSER_WS_URL") if !isRemote { return "", false diff --git a/k6ext/env_test.go b/env/env_test.go similarity index 97% rename from k6ext/env_test.go rename to env/env_test.go index 624c42210..13ad541aa 100644 --- a/k6ext/env_test.go +++ b/env/env_test.go @@ -1,4 +1,4 @@ -package k6ext +package env import ( "testing" @@ -11,7 +11,7 @@ func TestIsRemoteBrowser(t *testing.T) { testCases := []struct { name string - envLookup envLookupper + envLookup LookupFunc expIsRemote bool expValidWSURLs []string }{ diff --git a/examples/browser_args.js b/examples/browser_args.js deleted file mode 100644 index 1442308d7..000000000 --- a/examples/browser_args.js +++ /dev/null @@ -1,27 +0,0 @@ -import { check } from 'k6'; -import { chromium } from 'k6/x/browser'; - -export const options = { - thresholds: { - checks: ["rate==1.0"] - } -} - -export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - args: ['host-resolver-rules=MAP test.k6.io 127.0.0.1'], - }); - const context = browser.newContext(); - const page = context.newPage(); - - try { - const res = await page.goto('http://test.k6.io/', { waitUntil: 'load' }); - check(res, { - 'null response': r => r === null, - }); - } finally { - page.close(); - browser.close(); - } -} diff --git a/examples/browser_on.js b/examples/browser_on.js index b77522a46..bd37bd5db 100644 --- a/examples/browser_on.js +++ b/examples/browser_on.js @@ -2,15 +2,23 @@ import { check } from 'k6'; import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); check(browser, { 'should be connected after launch': browser.isConnected(), diff --git a/examples/colorscheme.js b/examples/colorscheme.js index deb659f82..2990c261a 100644 --- a/examples/colorscheme.js +++ b/examples/colorscheme.js @@ -2,6 +2,16 @@ import { check } from 'k6'; import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } @@ -10,9 +20,7 @@ export const options = { export default async function() { const preferredColorScheme = 'dark'; - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const context = browser.newContext({ // valid values are "light", "dark" or "no-preference" diff --git a/examples/device_emulation.js b/examples/device_emulation.js index 67656a62c..6a0c616f6 100644 --- a/examples/device_emulation.js +++ b/examples/device_emulation.js @@ -2,15 +2,23 @@ import { check, sleep } from 'k6'; import { chromium, devices } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const device = devices['iPhone X']; // The spread operator is currently unsupported by k6's Babel, so use diff --git a/examples/elementstate.js b/examples/elementstate.js index 458101bdf..e77fad4a7 100644 --- a/examples/elementstate.js +++ b/examples/elementstate.js @@ -2,15 +2,23 @@ import { check } from 'k6'; import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); diff --git a/examples/evaluate.js b/examples/evaluate.js index 31508b6c0..76a771904 100644 --- a/examples/evaluate.js +++ b/examples/evaluate.js @@ -2,35 +2,36 @@ import { check } from 'k6'; import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - ignoreDefaultArgs: ['--hide-scrollbars'] - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); try { await page.goto('https://test.k6.io/', { waitUntil: 'load' }); - const dimensions = page.evaluate(() => { - const obj = { - width: document.documentElement.clientWidth, - height: document.documentElement.clientHeight, - deviceScaleFactor: window.devicePixelRatio - }; - console.log(obj); // tests #120 - return obj; - }); - - check(dimensions, { - 'width': d => d.width === 1265, - 'height': d => d.height === 720, - 'scale': d => d.deviceScaleFactor === 1, + + const result = page.evaluate(([x, y]) => { + return Promise.resolve(x * y); + }, [5, 5]); + console.log(result); // tests #120 + + check(result, { + 'result is 25': (result) => result == 25, }); } finally { page.close(); diff --git a/examples/fillform.js b/examples/fillform.js index fa79c5b4e..e5b8d8005 100644 --- a/examples/fillform.js +++ b/examples/fillform.js @@ -2,15 +2,23 @@ import { check } from 'k6'; import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); diff --git a/examples/getattribute.js b/examples/getattribute.js index 2a5c392e6..004e21a1c 100644 --- a/examples/getattribute.js +++ b/examples/getattribute.js @@ -2,15 +2,23 @@ import { check } from 'k6'; import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); diff --git a/examples/grant_permission.js b/examples/grant_permission.js index a3dcd1786..889dcdab5 100644 --- a/examples/grant_permission.js +++ b/examples/grant_permission.js @@ -1,15 +1,23 @@ import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); // grant camera and microphone permissions to the // new browser context. diff --git a/examples/hosts.js b/examples/hosts.js index 22f9b8bf5..f33e8fa00 100644 --- a/examples/hosts.js +++ b/examples/hosts.js @@ -2,6 +2,16 @@ import { check } from 'k6'; import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, hosts: { 'test.k6.io': '127.0.0.254' }, thresholds: { checks: ["rate==1.0"] @@ -9,9 +19,7 @@ export const options = { }; export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); diff --git a/examples/keyboard.js b/examples/keyboard.js index 4b4b5eee1..a3def27e3 100644 --- a/examples/keyboard.js +++ b/examples/keyboard.js @@ -1,9 +1,20 @@ import { chromium } from 'k6/x/browser'; +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + } +} + export default async function () { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const page = browser.newPage(); await page.goto('https://test.k6.io/my_messages.php', { waitUntil: 'networkidle' }); diff --git a/examples/launch_naked.js b/examples/launch_naked.js index 101e74e50..b169bac6d 100644 --- a/examples/launch_naked.js +++ b/examples/launch_naked.js @@ -1,7 +1,18 @@ import exec from 'k6/execution'; import { chromium } from 'k6/x/browser'; -export const options = {} +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + } +} export default function() { try { diff --git a/examples/locator.js b/examples/locator.js index 99d90acea..ed3a248cb 100644 --- a/examples/locator.js +++ b/examples/locator.js @@ -1,15 +1,23 @@ import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); diff --git a/examples/locator_pom.js b/examples/locator_pom.js index f0447dc3f..636e83be6 100644 --- a/examples/locator_pom.js +++ b/examples/locator_pom.js @@ -1,6 +1,16 @@ import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } @@ -47,9 +57,7 @@ export class Bet { } export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); diff --git a/examples/mouse.js b/examples/mouse.js index 57f002f32..579f9fd95 100644 --- a/examples/mouse.js +++ b/examples/mouse.js @@ -1,9 +1,20 @@ import { chromium } from 'k6/x/browser'; +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + } +} + export default async function () { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const page = browser.newPage(); await page.goto('https://test.k6.io/', { waitUntil: 'networkidle' }); diff --git a/examples/multiple-scenario.js b/examples/multiple-scenario.js index 3c2afda12..1cffac388 100644 --- a/examples/multiple-scenario.js +++ b/examples/multiple-scenario.js @@ -7,6 +7,11 @@ export const options = { exec: 'messages', vus: 2, duration: '2s', + options: { + browser: { + type: 'chromium', + }, + }, }, news: { executor: 'per-vu-iterations', @@ -14,6 +19,11 @@ export const options = { vus: 2, iterations: 4, maxDuration: '5s', + options: { + browser: { + type: 'chromium', + }, + }, }, }, thresholds: { @@ -23,9 +33,7 @@ export const options = { } export async function messages() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const page = browser.newPage(); try { @@ -37,9 +45,7 @@ export async function messages() { } export async function news() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const page = browser.newPage(); try { diff --git a/examples/querying.js b/examples/querying.js index 26aac9cca..061d9eee4 100644 --- a/examples/querying.js +++ b/examples/querying.js @@ -2,15 +2,23 @@ import { check } from 'k6'; import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); diff --git a/examples/screenshot.js b/examples/screenshot.js index 360315864..8dfc07091 100644 --- a/examples/screenshot.js +++ b/examples/screenshot.js @@ -1,15 +1,23 @@ import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default async function() { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); diff --git a/examples/touchscreen.js b/examples/touchscreen.js index 414067df3..64bebf15a 100644 --- a/examples/touchscreen.js +++ b/examples/touchscreen.js @@ -1,9 +1,20 @@ import { chromium } from 'k6/x/browser'; +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + } +} + export default async function () { - const browser = chromium.launch({ - headless: __ENV.XK6_HEADLESS ? true : false, - }); + const browser = chromium.launch(); const page = browser.newPage(); await page.goto('https://test.k6.io/', { waitUntil: 'networkidle' }); diff --git a/examples/waitforfunction.js b/examples/waitforfunction.js index 299e6fe37..993fd1644 100644 --- a/examples/waitforfunction.js +++ b/examples/waitforfunction.js @@ -2,15 +2,23 @@ import { check } from 'k6'; import { chromium } from 'k6/x/browser'; export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, thresholds: { checks: ["rate==1.0"] } } export default async function() { - const browser = chromium.launch({ - headless: true, - }); + const browser = chromium.launch(); const context = browser.newContext(); const page = context.newPage(); diff --git a/go.mod b/go.mod index af742a2c2..4cfaab7de 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ go 1.19 require ( github.com/chromedp/cdproto v0.0.0-20221023212508-67ada9507fb2 - github.com/dop251/goja v0.0.0-20230216180835-5937a312edda + github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079 github.com/gorilla/websocket v1.5.0 github.com/mailru/easyjson v0.7.7 github.com/mccutchen/go-httpbin v1.1.2-0.20190116014521-c5cb2f4802fa github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.2 - go.k6.io/k6 v0.43.2-0.20230404074422-e40265226b89 + go.k6.io/k6 v0.44.2-0.20230509145227-d951eace0c16 golang.org/x/net v0.7.0 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde gopkg.in/guregu/null.v3 v3.3.0 @@ -28,6 +28,7 @@ require ( github.com/fatih/color v1.14.1 // indirect github.com/go-sourcemap/sourcemap v2.1.4-0.20211119122758-180fcef48034+incompatible // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.16.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -35,7 +36,7 @@ require ( github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/onsi/gomega v1.18.1 // indirect + github.com/onsi/gomega v1.20.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect github.com/spf13/afero v1.1.2 // indirect diff --git a/go.sum b/go.sum index bd165b701..8a4920625 100644 --- a/go.sum +++ b/go.sum @@ -8,23 +8,26 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/bufbuild/protocompile v0.2.1-0.20230123224550-da57cd758c2f h1:IXSA5gow10s7zIOJfPOpXDtNBWCTA0715BDAhoJBXEs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/chromedp/cdproto v0.0.0-20221023212508-67ada9507fb2 h1:xESwMZNYkDnZf9MUk+6lXfMbpDnEJwlEuIxKYKM1vJY= github.com/chromedp/cdproto v0.0.0-20221023212508-67ada9507fb2/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= -github.com/dop251/goja v0.0.0-20230216180835-5937a312edda h1:yWEvdMtib3RbPysHDTNf/c3gerF5r+iMcmhlAeE6hEk= -github.com/dop251/goja v0.0.0-20230216180835-5937a312edda/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= +github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079 h1:xkbJGxVnk5sM8/LXeTKaBOfAZrI+iqvIPyH8oK1c6CQ= +github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= @@ -32,6 +35,7 @@ github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8Wlg github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sourcemap/sourcemap v2.1.4-0.20211119122758-180fcef48034+incompatible h1:bopx7t9jyUNX1ebhr0G4gtQWmUOgwQRI0QsYhdYLgkU= github.com/go-sourcemap/sourcemap v2.1.4-0.20211119122758-180fcef48034+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= @@ -51,11 +55,18 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 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-redis v0.1.1 h1:rvWnLanRB2qzDwuY6NMBe6PXei3wJ3kjYvfCwRJ+q+8= +github.com/grafana/xk6-timers v0.1.2 h1:YVM6hPDgvy4SkdZQpd+/r9M0kDi1g+QdbSxW5ClfwDk= +github.com/grafana/xk6-webcrypto v0.1.0 h1:StrQZkUi4vo3bAMmBUHvIQ8P+zNKCH3AwN22TZdDwHs= +github.com/grafana/xk6-websockets v0.2.0 h1:oZcq4lm/p/Tc94ZMMNeYDML0DjU39jasC6kTyc6iF+8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/jhump/protoreflect v1.15.0 h1:U5T5/2LF0AZQFP9T4W5GfBjBaTruomrKobiR4E+oA/Q= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= @@ -80,6 +91,7 @@ github.com/mccutchen/go-httpbin v1.1.2-0.20190116014521-c5cb2f4802fa/go.mod h1:f github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd h1:AC3N94irbx2kWGA8f/2Ks7EQl2LxKIRQYuT9IJDwgiI= github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd/go.mod h1:9vRHVuLCjoFfE3GT06X0spdOAO+Zzo4AMjdIwUHBvAk= github.com/mstoykov/envconfig v1.4.1-0.20220114105314-765c6d8c76f1 h1:94EkGmhXrVUEal+uLwFUf4fMXPhZpM5tYxuIsxrCCbI= +github.com/mstoykov/k6-taskqueue-lib v0.1.0 h1:M3eww1HSOLEN6rIkbNOJHhOVhlqnqkhYj7GTieiMBz4= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -87,15 +99,12 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/onsi/gomega v1.20.2 h1:8uQq0zMgLEfa0vRrrBgaJF2gyW9Da9BmfGV+OyUzfkY= +github.com/onsi/gomega v1.20.2/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= @@ -124,8 +133,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.k6.io/k6 v0.43.2-0.20230404074422-e40265226b89 h1:aJSJixr/yPdHn7/bWP5sh1xrZMPJOjEYDgRNDS1sMbs= -go.k6.io/k6 v0.43.2-0.20230404074422-e40265226b89/go.mod h1:Azozhj76R5Fa1pPatdrTgl7+cL5JHBTIqp4aWroBMw4= +go.k6.io/k6 v0.44.2-0.20230509145227-d951eace0c16 h1:zBLEYAz7XCiiprlTv/Pk+pwgTuIbZf31Wej2yZ1yYXg= +go.k6.io/k6 v0.44.2-0.20230509145227-d951eace0c16/go.mod h1:4D2BnugW3gBWyI+yuKp18f+rFPkZIWBpxNe6Xe6MvGE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -140,7 +149,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= @@ -157,14 +165,13 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -178,6 +185,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= diff --git a/k6ext/context.go b/k6ext/context.go index 9c1de7698..f5f27193e 100644 --- a/k6ext/context.go +++ b/k6ext/context.go @@ -4,6 +4,7 @@ import ( "context" k6modules "go.k6.io/k6/js/modules" + k6lib "go.k6.io/k6/lib" "github.com/dop251/goja" ) @@ -51,3 +52,16 @@ func GetCustomMetrics(ctx context.Context) *CustomMetrics { func Runtime(ctx context.Context) *goja.Runtime { return GetVU(ctx).Runtime() } + +// GetScenarioOpts returns the browser options and environment variables associated +// with the given context. +func GetScenarioOpts(ctx context.Context, vu k6modules.VU) map[string]any { + ss := k6lib.GetScenarioState(ctx) + if ss == nil { + return nil + } + if so := vu.State().Options.Scenarios[ss.Name].GetScenarioOptions(); so != nil { + return so.Browser + } + return nil +} diff --git a/k6ext/k6test/doc.go b/k6ext/k6test/doc.go new file mode 100644 index 000000000..125b2c77d --- /dev/null +++ b/k6ext/k6test/doc.go @@ -0,0 +1,2 @@ +// Package k6test provides mock implementations of k6 elements for testing purposes. +package k6test diff --git a/k6ext/k6test/executor.go b/k6ext/k6test/executor.go new file mode 100644 index 000000000..b1e332e4f --- /dev/null +++ b/k6ext/k6test/executor.go @@ -0,0 +1,34 @@ +package k6test + +import ( + "github.com/sirupsen/logrus" + + k6lib "go.k6.io/k6/lib" + k6executor "go.k6.io/k6/lib/executor" +) + +// TestExecutor is a k6lib.ExecutorConfig implementation +// for testing purposes. +type TestExecutor struct { + k6executor.BaseConfig +} + +// GetDescription returns a mock Executor description. +func (te *TestExecutor) GetDescription(*k6lib.ExecutionTuple) string { + return "TestExecutor" +} + +// GetExecutionRequirements is a dummy implementation that just returns nil. +func (te *TestExecutor) GetExecutionRequirements(*k6lib.ExecutionTuple) []k6lib.ExecutionStep { + return nil +} + +// NewExecutor is a dummy implementation that just returns nil. +func (te *TestExecutor) NewExecutor(*k6lib.ExecutionState, *logrus.Entry) (k6lib.Executor, error) { + return nil, nil +} + +// HasWork is a dummy implementation that returns true. +func (te *TestExecutor) HasWork(*k6lib.ExecutionTuple) bool { + return true +} diff --git a/k6ext/k6test/vu.go b/k6ext/k6test/vu.go index abb340c18..58b0248be 100644 --- a/k6ext/k6test/vu.go +++ b/k6ext/k6test/vu.go @@ -3,17 +3,18 @@ package k6test import ( "testing" + "github.com/dop251/goja" + "github.com/stretchr/testify/require" + "gopkg.in/guregu/null.v3" + "github.com/grafana/xk6-browser/k6ext" k6eventloop "go.k6.io/k6/js/eventloop" k6modulestest "go.k6.io/k6/js/modulestest" k6lib "go.k6.io/k6/lib" + k6executor "go.k6.io/k6/lib/executor" k6testutils "go.k6.io/k6/lib/testutils" k6metrics "go.k6.io/k6/metrics" - - "github.com/dop251/goja" - "github.com/stretchr/testify/require" - "gopkg.in/guregu/null.v3" ) // VU is a k6 VU instance. @@ -81,6 +82,17 @@ func NewVU(tb testing.TB, opts ...any) *VU { Batch: null.IntFrom(20), BatchPerHost: null.IntFrom(20), // HTTPDebug: null.StringFrom("full"), + Scenarios: k6lib.ScenarioConfigs{ + "default": &TestExecutor{ + BaseConfig: k6executor.BaseConfig{ + Options: &k6lib.ScenarioOptions{ + Browser: map[string]any{ + "type": "chromium", + }, + }, + }, + }, + }, }, Logger: k6testutils.NewLogger(tb), Group: root, @@ -91,6 +103,7 @@ func NewVU(tb testing.TB, opts ...any) *VU { } ctx := k6ext.WithVU(testRT.VU.CtxField, testRT.VU) + ctx = k6lib.WithScenarioState(ctx, &k6lib.ScenarioState{Name: "default"}) testRT.VU.CtxField = ctx return &VU{VU: testRT.VU, Loop: testRT.EventLoop, toBeState: state, samples: samples} diff --git a/tests/browser_test.go b/tests/browser_test.go index e1df565a9..8459f49d4 100644 --- a/tests/browser_test.go +++ b/tests/browser_test.go @@ -1,7 +1,6 @@ package tests import ( - "context" "os" "path/filepath" "regexp" @@ -104,16 +103,15 @@ func TestBrowserOn(t *testing.T) { t.Parallel() var ( - ctx, cancel = context.WithCancel(context.Background()) - b = newTestBrowser(t, ctx) - rt = b.vu.Runtime() - log []string + b = newTestBrowser(t) + rt = b.vu.Runtime() + log []string ) require.NoError(t, rt.Set("b", b.Browser)) require.NoError(t, rt.Set("log", func(s string) { log = append(log, s) })) - time.AfterFunc(100*time.Millisecond, cancel) + time.AfterFunc(100*time.Millisecond, b.Cancel) _, err := b.runJavaScript(script, "disconnected") assert.ErrorContains(t, err, "browser.on promise rejected: context canceled") }) @@ -146,8 +144,8 @@ func TestBrowserCrashErr(t *testing.T) { t.Parallel() assertExceptionContains(t, goja.New(), func() { - lopts := defaultLaunchOpts() - lopts.Args = []any{"remote-debugging-port=99999"} + lopts := defaultBrowserOpts() + lopts.Args = []string{"remote-debugging-port=99999"} newTestBrowser(t, lopts) }, "launching browser: Invalid devtools server port") @@ -231,13 +229,13 @@ func TestMultiConnectToSingleBrowser(t *testing.T) { tb := newTestBrowser(t, withSkipClose()) defer tb.Close() - b1 := tb.browserType.Connect(tb.wsURL, nil) + b1 := tb.browserType.Connect(tb.wsURL) bctx1, err := b1.NewContext(nil) require.NoError(t, err) p1, err := bctx1.NewPage() require.NoError(t, err, "failed to create page #1") - b2 := tb.browserType.Connect(tb.wsURL, nil) + b2 := tb.browserType.Connect(tb.wsURL) bctx2, err := b2.NewContext(nil) require.NoError(t, err) diff --git a/tests/browser_type_test.go b/tests/browser_type_test.go index 9de3ce312..1d492a0c3 100644 --- a/tests/browser_type_test.go +++ b/tests/browser_type_test.go @@ -18,7 +18,7 @@ func TestBrowserTypeConnect(t *testing.T) { bt := chromium.NewBrowserType(vu) vu.MoveToVUContext() - b := bt.Connect(tb.wsURL, nil) + b := bt.Connect(tb.wsURL) b.NewPage(nil) } diff --git a/tests/doc.go b/tests/doc.go new file mode 100644 index 000000000..75d6372de --- /dev/null +++ b/tests/doc.go @@ -0,0 +1,2 @@ +// Package tests contains integration tests. +package tests diff --git a/tests/frame_test.go b/tests/frame_test.go index 4dfc4f8b9..4fab0cff9 100644 --- a/tests/frame_test.go +++ b/tests/frame_test.go @@ -84,7 +84,7 @@ func TestFrameNoPanicWithEmbeddedIFrame(t *testing.T) { t.Parallel() - opts := defaultLaunchOpts() + opts := defaultBrowserOpts() opts.Headless = false tb := newTestBrowser(t, withFileServer(), opts) p := tb.NewPage(nil) diff --git a/tests/launch_options_slowmo_test.go b/tests/launch_options_slowmo_test.go index 1f7a9c457..d297a1cb4 100644 --- a/tests/launch_options_slowmo_test.go +++ b/tests/launch_options_slowmo_test.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/xk6-browser/common" ) -func TestLaunchOptionsSlowMo(t *testing.T) { +func TestBrowserOptionsSlowMo(t *testing.T) { t.Parallel() if testing.Short() { diff --git a/tests/page_test.go b/tests/page_test.go index f097e0e0a..75508cfca 100644 --- a/tests/page_test.go +++ b/tests/page_test.go @@ -2,7 +2,6 @@ package tests import ( "bytes" - "context" _ "embed" "encoding/json" "fmt" @@ -645,10 +644,10 @@ func TestPageWaitForLoadState(t *testing.T) { // See: The issue #187 for details. func TestPageWaitForNavigationErrOnCtxDone(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - p := newTestBrowser(t, ctx).NewPage(nil) - go cancel() - <-ctx.Done() + b := newTestBrowser(t) + p := b.NewPage(nil) + go b.Cancel() + <-b.Context().Done() _, err := p.WaitForNavigation(nil) require.ErrorContains(t, err, "canceled") } diff --git a/tests/test_browser.go b/tests/test_browser.go index 68824378b..67127f7bd 100644 --- a/tests/test_browser.go +++ b/tests/test_browser.go @@ -6,12 +6,14 @@ import ( "net/http" "os" "strconv" + "strings" "testing" "time" "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/chromium" "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/env" "github.com/grafana/xk6-browser/k6ext" "github.com/grafana/xk6-browser/k6ext/k6test" @@ -39,20 +41,21 @@ type testBrowser struct { browserType api.BrowserType api.Browser + + cancel context.CancelFunc } // newTestBrowser configures and launches a new chrome browser. // It automatically closes it when `t` returns. // // opts provides a way to customize the newTestBrowser. -// see: withLaunchOptions for an example. +// see: withBrowserOptions for an example. func newTestBrowser(tb testing.TB, opts ...any) *testBrowser { tb.Helper() // set default options and then customize them var ( - ctx context.Context - launchOpts = defaultLaunchOpts() + browserOpts = defaultBrowserOpts() enableHTTPMultiBin = false enableFileServer = false enableLogCache = false @@ -61,8 +64,8 @@ func newTestBrowser(tb testing.TB, opts ...any) *testBrowser { ) for _, opt := range opts { switch opt := opt.(type) { - case withLaunchOptions: - launchOpts = opt + case withBrowserOptions: + browserOpts = opt case httpServerOption: enableHTTPMultiBin = true case fileServerOption: @@ -70,8 +73,6 @@ func newTestBrowser(tb testing.TB, opts ...any) *testBrowser { enableHTTPMultiBin = true case logCacheOption: enableLogCache = true - case withContext: - ctx = opt case skipCloseOption: skipClose = true case withSamplesListener: @@ -81,15 +82,9 @@ func newTestBrowser(tb testing.TB, opts ...any) *testBrowser { vu := setupHTTPTestModuleInstance(tb, samples) - if ctx == nil { - dummyCtx, cancel := context.WithCancel(vu.Context()) - tb.Cleanup(cancel) - vu.CtxField = dummyCtx - } else { - // Attach the mock VU to the passed context - ctx = k6ext.WithVU(ctx, vu) - vu.CtxField = ctx - } + dummyCtx, cancel := context.WithCancel(vu.Context()) + tb.Cleanup(cancel) + vu.CtxField = dummyCtx registry := k6metrics.NewRegistry() k6m := k6ext.RegisterCustomMetrics(registry) @@ -105,7 +100,6 @@ func newTestBrowser(tb testing.TB, opts ...any) *testBrowser { var ( testServer *k6httpmultibin.HTTPMultiBin state = vu.StateField - rt = vu.RuntimeField lc *logCache ) @@ -118,7 +112,9 @@ func newTestBrowser(tb testing.TB, opts ...any) *testBrowser { state.Transport = testServer.HTTPTransport } - b, pid := bt.Launch(rt.ToValue(launchOpts)) + bt.SetEnvLookupper(setupEnvLookupper(tb, browserOpts)) + + b, pid := bt.Launch() cb, ok := b.(*common.Browser) if !ok { tb.Fatalf("testBrowser: unexpected browser %T", b) @@ -136,7 +132,7 @@ func newTestBrowser(tb testing.TB, opts ...any) *testBrowser { tbr := &testBrowser{ t: tb, - ctx: bt.Ctx, // This context has the additional wrapping of common.WithLaunchOptions + ctx: bt.Ctx, // This context has the additional wrapping of common.WithBrowserOptions http: testServer, vu: vu, logCache: lc, @@ -144,6 +140,7 @@ func newTestBrowser(tb testing.TB, opts ...any) *testBrowser { browserType: bt, pid: pid, wsURL: cb.WsURL(), + cancel: cancel, } if enableFileServer { tbr = tbr.withFileServer() @@ -212,6 +209,16 @@ func (b *testBrowser) staticURL(path string) string { return b.URL("/" + testBrowserStaticDir + "/" + path) } +// Context returns the testBrowser context. +func (b *testBrowser) Context() context.Context { + return b.ctx +} + +// Cancel cancels the testBrowser context. +func (b *testBrowser) Cancel() { + b.cancel() +} + // attachFrame attaches the frame to the page and returns it. func (b *testBrowser) attachFrame(page api.Page, frameID string, url string) api.Frame { b.t.Helper() @@ -382,38 +389,36 @@ func (t testPromise) then(resolve any, reject ...any) testPromise { return t.tb.promise(p) } -// launchOptions provides a way to customize browser type -// launch options in tests. -type launchOptions struct { - Args []any `js:"args"` - Debug bool `js:"debug"` - Headless bool `js:"headless"` - SlowMo string `js:"slowMo"` - Timeout string `js:"timeout"` +// browserOptions provides a way to customize browser +// options in tests. +type browserOptions struct { + Args []string `js:"args"` + Debug bool `js:"debug"` + Headless bool `js:"headless"` + Timeout string `js:"timeout"` } -// withLaunchOptions is a helper for increasing readability -// in tests while customizing the browser type launch options. +// withBrowserOptions is a helper for increasing readability +// in tests while customizing the browser options. // // example: // -// b := TestBrowser(t, withLaunchOptions{ +// b := TestBrowser(t, withBrowserOptions{ // SlowMo: "100s", // Timeout: "30s", // }) -type withLaunchOptions = launchOptions +type withBrowserOptions = browserOptions -// defaultLaunchOptions returns defaults for browser type launch options. +// defaultBrowserOpts returns defaults for browser options. // TestBrowser uses this for launching a browser type by default. -func defaultLaunchOpts() launchOptions { +func defaultBrowserOpts() browserOptions { headless := true if v, found := os.LookupEnv("XK6_BROWSER_TEST_HEADLESS"); found { headless, _ = strconv.ParseBool(v) } - return launchOptions{ + return browserOptions{ Headless: headless, - SlowMo: "0s", Timeout: "30s", } } @@ -447,10 +452,6 @@ func withFileServer() fileServerOption { return struct{}{} } -// withContext is used to detect whether to use a custom context in the test -// browser. -type withContext = context.Context - // logCacheOption is used to detect whether to enable the log cache. type logCacheOption struct{} @@ -495,3 +496,26 @@ func setupHTTPTestModuleInstance(tb testing.TB, samples chan k6metrics.SampleCon return vu } + +func setupEnvLookupper(tb testing.TB, opts browserOptions) env.LookupFunc { + tb.Helper() + + return func(key string) (string, bool) { + switch key { + case "K6_BROWSER_ARGS": + if len(opts.Args) != 0 { + return strings.Join(opts.Args, ","), true + } + case "K6_BROWSER_DEBUG": + return strconv.FormatBool(opts.Debug), true + case "K6_BROWSER_HEADLESS": + return strconv.FormatBool(opts.Headless), true + case "K6_BROWSER_TIMEOUT": + if opts.Timeout != "" { + return opts.Timeout, true + } + } + + return "", false + } +}