diff --git a/common/element_handle.go b/common/element_handle.go index e612920e5..72bb94f84 100644 --- a/common/element_handle.go +++ b/common/element_handle.go @@ -17,6 +17,8 @@ import ( "github.com/grafana/xk6-browser/common/js" "github.com/grafana/xk6-browser/k6ext" + + k6common "go.k6.io/k6/js/common" ) const ( @@ -479,7 +481,7 @@ func (h *ElementHandle) press(apiCtx context.Context, key string, opts KeyboardO //nolint:funlen,gocognit,cyclop func (h *ElementHandle) selectOption(apiCtx context.Context, values sobek.Value) (any, error) { convertSelectOptionValues := func(values sobek.Value) ([]any, error) { - if sobek.IsNull(values) || sobek.IsUndefined(values) { + if k6common.IsNullish(values) { return nil, nil } @@ -489,38 +491,41 @@ func (h *ElementHandle) selectOption(apiCtx context.Context, values sobek.Value) rt = h.execCtx.vu.Runtime() ) switch values.ExportType().Kind() { - case reflect.Map: - s := reflect.ValueOf(t) - for i := 0; i < s.Len(); i++ { - item := s.Index(i) - switch item.Kind() { - case reflect.TypeOf(nil).Kind(): - return nil, fmt.Errorf("options[%d]: expected object, got null", i) - case reflect.TypeOf(&ElementHandle{}).Kind(): - opts = append(opts, t.(*ElementHandle)) - case reflect.TypeOf(sobek.Object{}).Kind(): - obj := values.ToObject(rt) - opt := SelectOption{} - for _, k := range obj.Keys() { - switch k { - case "value": - opt.Value = new(string) - *opt.Value = obj.Get(k).String() - case "label": - opt.Label = new(string) - *opt.Label = obj.Get(k).String() - case "index": - opt.Index = new(int64) - *opt.Index = obj.Get(k).ToInteger() - } - } - opts = append(opts, &opt) - case reflect.String: + case reflect.Slice: + var sl []interface{} + if err := rt.ExportTo(values, &sl); err != nil { + return nil, fmt.Errorf("options: expected array, got %T", values) + } + + for _, item := range sl { + switch item := item.(type) { + case string: opt := SelectOption{Value: new(string)} - *opt.Value = item.String() + *opt.Value = item opts = append(opts, &opt) + case map[string]interface{}: + opt, err := extractSelectOptionFromMap(item) + if err != nil { + return nil, err + } + + opts = append(opts, opt) + default: + return nil, fmt.Errorf("options: expected string or object, got %T", item) } } + case reflect.Map: + var raw map[string]interface{} + if err := rt.ExportTo(values, &raw); err != nil { + return nil, fmt.Errorf("options: expected object, got %T", values) + } + + opt, err := extractSelectOptionFromMap(raw) + if err != nil { + return nil, err + } + + opts = append(opts, opt) case reflect.TypeOf(&ElementHandle{}).Kind(): opts = append(opts, t.(*ElementHandle)) case reflect.TypeOf(sobek.Object{}).Kind(): @@ -544,6 +549,8 @@ func (h *ElementHandle) selectOption(apiCtx context.Context, values sobek.Value) opt := SelectOption{Value: new(string)} *opt.Value = t.(string) opts = append(opts, &opt) + default: + return nil, fmt.Errorf("options: unsupported type %T", values) } return opts, nil @@ -575,6 +582,44 @@ func (h *ElementHandle) selectOption(apiCtx context.Context, values sobek.Value) return result, nil } +func extractSelectOptionFromMap(v map[string]interface{}) (*SelectOption, error) { + opt := &SelectOption{} + for k, raw := range v { + switch k { + case "value": + opt.Value = new(string) + + v, ok := raw.(string) + if !ok { + return nil, fmt.Errorf("options[%v]: expected string, got %T", k, raw) + } + + *opt.Value = v + case "label": + opt.Label = new(string) + + v, ok := raw.(string) + if !ok { + return nil, fmt.Errorf("options[%v]: expected string, got %T", k, raw) + } + *opt.Label = v + case "index": + opt.Index = new(int64) + + switch raw := raw.(type) { + case int: + *opt.Index = int64(raw) + case int64: + *opt.Index = raw + default: + return nil, fmt.Errorf("options[%v]: expected int, got %T", k, raw) + } + } + } + + return opt, nil +} + func (h *ElementHandle) selectText(apiCtx context.Context) error { fn := ` (node, injected) => { diff --git a/tests/locator_test.go b/tests/locator_test.go index 0421c841b..3d5cca199 100644 --- a/tests/locator_test.go +++ b/tests/locator_test.go @@ -665,3 +665,60 @@ func TestLocatorShadowDOM(t *testing.T) { err = p.Click("#inner-link", common.NewFrameClickOptions(time.Second)) require.NoError(t, err) } + +func TestSelectOption(t *testing.T) { + t.Parallel() + + tb := newTestBrowser(t, + withFileServer(), + ) + defer tb.Browser.Close() + + vu, _, _, cleanUp := startIteration(t) + defer cleanUp() + + got := vu.RunPromise(t, ` + const page = await browser.newPage(); + + await page.goto('%s'); + + const options = page.locator('#numbers-options'); + + await options.selectOption({label:'Five'}); + let selectedValue = await options.inputValue(); + if (selectedValue !== 'five') { + throw new Error('Expected "five" but got ' + selectedValue); + } + + await options.selectOption({index:5}); + selectedValue = await options.inputValue(); + if (selectedValue !== 'five') { + throw new Error('Expected "five" but got ' + selectedValue); + } + + await options.selectOption({value:'four'}); + selectedValue = await options.inputValue(); + if (selectedValue !== 'four') { + throw new Error('Expected "four" but got ' + selectedValue); + } + + await options.selectOption([{label:'One'}]); + selectedValue = await options.inputValue(); + if (selectedValue !== 'one') { + throw new Error('Expected "one" but got ' + selectedValue); + } + + await options.selectOption(['two']); // Value + selectedValue = await options.inputValue(); + if (selectedValue !== 'two') { + throw new Error('Expected "two" but got ' + selectedValue); + } + + await options.selectOption('five'); // Value + selectedValue = await options.inputValue(); + if (selectedValue !== 'five') { + throw new Error('Expected "five" but got ' + selectedValue); + } + `, tb.staticURL("select_options.html")) + assert.Equal(t, sobek.Undefined(), got.Result()) +} diff --git a/tests/static/select_options.html b/tests/static/select_options.html new file mode 100644 index 000000000..ea854941d --- /dev/null +++ b/tests/static/select_options.html @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file