diff --git a/browser/element_handle_mapping.go b/browser/element_handle_mapping.go index 170a8e020..afaeba1db 100644 --- a/browser/element_handle_mapping.go +++ b/browser/element_handle_mapping.go @@ -65,7 +65,14 @@ func mapElementHandle(vu moduleVU, eh *common.ElementHandle) mapping { //nolint: }, "getAttribute": func(name string) *goja.Promise { return k6ext.Promise(vu.Context(), func() (any, error) { - return eh.GetAttribute(name) //nolint:wrapcheck + s, ok, err := eh.GetAttribute(name) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil }) }, "hover": func(opts goja.Value) *goja.Promise { diff --git a/browser/frame_mapping.go b/browser/frame_mapping.go index 6c6704ab1..53b9fb671 100644 --- a/browser/frame_mapping.go +++ b/browser/frame_mapping.go @@ -94,7 +94,14 @@ func mapFrame(vu moduleVU, f *common.Frame) mapping { //nolint:gocognit,cyclop }, "getAttribute": func(selector, name string, opts goja.Value) *goja.Promise { return k6ext.Promise(vu.Context(), func() (any, error) { - return f.GetAttribute(selector, name, opts) //nolint:wrapcheck + s, ok, err := f.GetAttribute(selector, name, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil }) }, "goto": func(url string, opts goja.Value) (*goja.Promise, error) { diff --git a/browser/locator_mapping.go b/browser/locator_mapping.go index 88b81b7ac..d0d73e674 100644 --- a/browser/locator_mapping.go +++ b/browser/locator_mapping.go @@ -88,7 +88,14 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping { //nolint:funlen }, "getAttribute": func(name string, opts goja.Value) *goja.Promise { return k6ext.Promise(vu.Context(), func() (any, error) { - return lo.GetAttribute(name, opts) //nolint:wrapcheck + s, ok, err := lo.GetAttribute(name, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil }) }, "innerHTML": func(opts goja.Value) *goja.Promise { diff --git a/browser/mapping_test.go b/browser/mapping_test.go index 037e4fddb..56debea1b 100644 --- a/browser/mapping_test.go +++ b/browser/mapping_test.go @@ -308,7 +308,7 @@ type pageAPI interface { Fill(selector string, value string, opts goja.Value) error Focus(selector string, opts goja.Value) error Frames() []*common.Frame - GetAttribute(selector string, name string, opts goja.Value) (any, error) + GetAttribute(selector string, name string, opts goja.Value) (string, bool, error) GetKeyboard() *common.Keyboard GetMouse() *common.Mouse GetTouchscreen() *common.Touchscreen @@ -380,7 +380,7 @@ type frameAPI interface { Fill(selector string, value string, opts goja.Value) error Focus(selector string, opts goja.Value) error FrameElement() (*common.ElementHandle, error) - GetAttribute(selector string, name string, opts goja.Value) (any, error) + GetAttribute(selector string, name string, opts goja.Value) (string, bool, error) Goto(url string, opts goja.Value) (*common.Response, error) Hover(selector string, opts goja.Value) error InnerHTML(selector string, opts goja.Value) (string, error) @@ -430,7 +430,7 @@ type elementHandleAPI interface { DispatchEvent(typ string, props goja.Value) error Fill(value string, opts goja.Value) error Focus() error - GetAttribute(name string) (any, error) + GetAttribute(name string) (string, bool, error) Hover(opts goja.Value) error InnerHTML() (string, error) InnerText() (string, error) @@ -512,7 +512,7 @@ type locatorAPI interface { IsHidden(opts goja.Value) (bool, error) Fill(value string, opts goja.Value) error Focus(opts goja.Value) error - GetAttribute(name string, opts goja.Value) (any, error) + GetAttribute(name string, opts goja.Value) (string, bool, error) InnerHTML(opts goja.Value) (string, error) InnerText(opts goja.Value) (string, error) TextContent(opts goja.Value) (string, error) diff --git a/browser/page_mapping.go b/browser/page_mapping.go index 28be0e234..b7fba111a 100644 --- a/browser/page_mapping.go +++ b/browser/page_mapping.go @@ -112,7 +112,14 @@ func mapPage(vu moduleVU, p *common.Page) mapping { //nolint:gocognit,cyclop }, "getAttribute": func(selector string, name string, opts goja.Value) *goja.Promise { return k6ext.Promise(vu.Context(), func() (any, error) { - return p.GetAttribute(selector, name, opts) //nolint:wrapcheck + s, ok, err := p.GetAttribute(selector, name, opts) + if err != nil { + return nil, err //nolint:wrapcheck + } + if !ok { + return nil, nil + } + return s, nil }) }, "goto": func(url string, opts goja.Value) (*goja.Promise, error) { diff --git a/common/element_handle.go b/common/element_handle.go index a4152e8e7..6c14eccb0 100644 --- a/common/element_handle.go +++ b/common/element_handle.go @@ -840,7 +840,8 @@ func (h *ElementHandle) Focus() error { } // GetAttribute retrieves the value of specified element attribute. -func (h *ElementHandle) GetAttribute(name string) (any, error) { +// The second return value is true if the attribute exists, and false otherwise. +func (h *ElementHandle) GetAttribute(name string) (string, bool, error) { getAttribute := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.getAttribute(apiCtx, name) } @@ -851,12 +852,20 @@ func (h *ElementHandle) GetAttribute(name string) (any, error) { v, err := call(h.ctx, getAttributeAction, opts.Timeout) if err != nil { - return nil, fmt.Errorf("getting attribute %q of element: %w", name, err) + return "", false, fmt.Errorf("getting attribute %q of element: %w", name, err) + } + if v == nil { + return "", false, nil + } + s, ok := v.(string) + if !ok { + return "", false, fmt.Errorf( + "getting attribute %q of element: unexpected type %T (expecting string)", + name, v, + ) } - applySlowMo(h.ctx) - - return v, nil + return s, true, nil } // Hover scrolls element into view and hovers over its center point. diff --git a/common/frame.go b/common/frame.go index 2cde039ee..d95da549e 100644 --- a/common/frame.go +++ b/common/frame.go @@ -960,22 +960,23 @@ func (f *Frame) FrameElement() (*ElementHandle, error) { } // GetAttribute of the first element found that matches the selector. -func (f *Frame) GetAttribute(selector, name string, opts goja.Value) (any, error) { +// The second return value is true if the attribute exists, and false otherwise. +func (f *Frame) GetAttribute(selector, name string, opts goja.Value) (string, bool, error) { f.log.Debugf("Frame:GetAttribute", "fid:%s furl:%q sel:%q name:%s", f.ID(), f.URL(), selector, name) popts := NewFrameBaseOptions(f.defaultTimeout()) if err := popts.Parse(f.ctx, opts); err != nil { - return nil, fmt.Errorf("parsing get attribute options: %w", err) + return "", false, fmt.Errorf("parsing get attribute options: %w", err) } - v, err := f.getAttribute(selector, name, popts) + s, ok, err := f.getAttribute(selector, name, popts) if err != nil { - return nil, fmt.Errorf("getting attribute %q of %q: %w", name, selector, err) + return "", false, fmt.Errorf("getting attribute %q of %q: %w", name, selector, err) } - return v, nil + return s, ok, nil } -func (f *Frame) getAttribute(selector, name string, opts *FrameBaseOptions) (any, error) { +func (f *Frame) getAttribute(selector, name string, opts *FrameBaseOptions) (string, bool, error) { getAttribute := func(apiCtx context.Context, handle *ElementHandle) (any, error) { return handle.getAttribute(apiCtx, name) } @@ -985,10 +986,17 @@ func (f *Frame) getAttribute(selector, name string, opts *FrameBaseOptions) (any ) v, err := call(f.ctx, act, opts.Timeout) if err != nil { - return "", errorFromDOMError(err) + return "", false, errorFromDOMError(err) + } + if v == nil { + return "", false, nil + } + s, ok := v.(string) + if !ok { + return "", false, fmt.Errorf("unexpected type %T (expecting string)", v) } - return v, nil + return s, true, nil } // Referrer returns the referrer of the frame from the network manager diff --git a/common/locator.go b/common/locator.go index 0e479a700..dacd8eb4f 100644 --- a/common/locator.go +++ b/common/locator.go @@ -322,7 +322,8 @@ func (l *Locator) focus(opts *FrameBaseOptions) error { } // GetAttribute of the element using locator's selector with strict mode on. -func (l *Locator) GetAttribute(name string, opts goja.Value) (any, error) { +// The second return value is true if the attribute exists, and false otherwise. +func (l *Locator) GetAttribute(name string, opts goja.Value) (string, bool, error) { l.log.Debugf( "Locator:GetAttribute", "fid:%s furl:%q sel:%q name:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, name, opts, @@ -330,17 +331,17 @@ func (l *Locator) GetAttribute(name string, opts goja.Value) (any, error) { copts := NewFrameBaseOptions(l.frame.defaultTimeout()) if err := copts.Parse(l.ctx, opts); err != nil { - return nil, fmt.Errorf("parsing get attribute options: %w", err) + return "", false, fmt.Errorf("parsing get attribute options: %w", err) } - v, err := l.getAttribute(name, copts) + s, ok, err := l.getAttribute(name, copts) if err != nil { - return nil, fmt.Errorf("getting attribute %q of %q: %w", name, l.selector, err) + return "", false, fmt.Errorf("getting attribute %q of %q: %w", name, l.selector, err) } - return v, nil + return s, ok, nil } -func (l *Locator) getAttribute(name string, opts *FrameBaseOptions) (any, error) { +func (l *Locator) getAttribute(name string, opts *FrameBaseOptions) (string, bool, error) { opts.Strict = true return l.frame.getAttribute(l.selector, name, opts) } diff --git a/common/page.go b/common/page.go index aebb02342..5de49397e 100644 --- a/common/page.go +++ b/common/page.go @@ -821,7 +821,8 @@ func (p *Page) Frames() []*Frame { } // GetAttribute returns the attribute value of the element matching the provided selector. -func (p *Page) GetAttribute(selector string, name string, opts goja.Value) (any, error) { +// The second return value is true if the attribute exists, and false otherwise. +func (p *Page) GetAttribute(selector string, name string, opts goja.Value) (string, bool, error) { p.logger.Debugf("Page:GetAttribute", "sid:%v selector:%s name:%s", p.sessionID(), selector, name) diff --git a/tests/element_handle_test.go b/tests/element_handle_test.go index 7fc00e9f6..df03719b3 100644 --- a/tests/element_handle_test.go +++ b/tests/element_handle_test.go @@ -248,20 +248,49 @@ func TestElementHandleNonClickable(t *testing.T) { func TestElementHandleGetAttribute(t *testing.T) { t.Parallel() - const want = "https://somewhere" + p := newTestBrowser(t).NewPage(nil) + err := p.SetContent(`Something`, nil) + require.NoError(t, err) + + el, err := p.Query("#el") + require.NoError(t, err) + + got, ok, err := el.GetAttribute("href") + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, "null", got) +} + +func TestElementHandleGetAttributeMissing(t *testing.T) { + t.Parallel() p := newTestBrowser(t).NewPage(nil) - err := p.SetContent(` - Dark - `, nil) + err := p.SetContent(`Something`, nil) + require.NoError(t, err) + + el, err := p.Query("#el") + require.NoError(t, err) + + got, ok, err := el.GetAttribute("missing") + require.NoError(t, err) + require.False(t, ok) + assert.Equal(t, "", got) +} + +func TestElementHandleGetAttributeEmpty(t *testing.T) { + t.Parallel() + + p := newTestBrowser(t).NewPage(nil) + err := p.SetContent(`Something`, nil) require.NoError(t, err) - el, err := p.Query("#dark-mode-toggle-X") + el, err := p.Query("#el") require.NoError(t, err) - got, err := el.GetAttribute("href") + got, ok, err := el.GetAttribute("empty") require.NoError(t, err) - assert.Equal(t, want, got) + require.True(t, ok) + assert.Equal(t, "", got) } func TestElementHandleInputValue(t *testing.T) { diff --git a/tests/frame_test.go b/tests/frame_test.go index eb6bc44cb..63f780839 100644 --- a/tests/frame_test.go +++ b/tests/frame_test.go @@ -167,3 +167,42 @@ func TestFrameTitle(t *testing.T) { require.NoError(t, err) assert.Equal(t, "Some title", p.MainFrame().Title()) } + +func TestFrameGetAttribute(t *testing.T) { + t.Parallel() + + p := newTestBrowser(t).NewPage(nil) + err := p.SetContent(`Something`, nil) + require.NoError(t, err) + + got, ok, err := p.Frames()[0].GetAttribute("#el", "href", nil) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, "null", got) +} + +func TestFrameGetAttributeMissing(t *testing.T) { + t.Parallel() + + p := newTestBrowser(t).NewPage(nil) + err := p.SetContent(`Something`, nil) + require.NoError(t, err) + + got, ok, err := p.Frames()[0].GetAttribute("#el", "missing", nil) + require.NoError(t, err) + require.False(t, ok) + assert.Equal(t, "", got) +} + +func TestFrameGetAttributeEmpty(t *testing.T) { + t.Parallel() + + p := newTestBrowser(t).NewPage(nil) + err := p.SetContent(`Something`, nil) + require.NoError(t, err) + + got, ok, err := p.Frames()[0].GetAttribute("#el", "empty", nil) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, "", got) +} diff --git a/tests/locator_test.go b/tests/locator_test.go index 8885fa066..c92c66921 100644 --- a/tests/locator_test.go +++ b/tests/locator_test.go @@ -162,9 +162,10 @@ func TestLocator(t *testing.T) { { "GetAttribute", func(_ *testBrowser, p *common.Page) { l := p.Locator("#inputText", nil) - v, err := l.GetAttribute("value", nil) + v, ok, err := l.GetAttribute("value", nil) require.NoError(t, err) require.NotNil(t, v) + require.True(t, ok) require.Equal(t, "something", v) }, }, @@ -366,7 +367,7 @@ func TestLocator(t *testing.T) { }, { "GetAttribute", func(l *common.Locator, tb *testBrowser) error { - _, err := l.GetAttribute("value", timeout(tb)) + _, _, err := l.GetAttribute("value", timeout(tb)) return err }, }, diff --git a/tests/page_test.go b/tests/page_test.go index 50a97f904..1fae03428 100644 --- a/tests/page_test.go +++ b/tests/page_test.go @@ -1806,3 +1806,42 @@ func TestPageTargetBlank(t *testing.T) { require.NoError(t, err) assert.Equal(t, "you clicked!", got) } + +func TestPageGetAttribute(t *testing.T) { + t.Parallel() + + p := newTestBrowser(t).NewPage(nil) + err := p.SetContent(`Something`, nil) + require.NoError(t, err) + + got, ok, err := p.GetAttribute("#el", "href", nil) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, "null", got) +} + +func TestPageGetAttributeMissing(t *testing.T) { + t.Parallel() + + p := newTestBrowser(t).NewPage(nil) + err := p.SetContent(`Something`, nil) + require.NoError(t, err) + + got, ok, err := p.GetAttribute("#el", "missing", nil) + require.NoError(t, err) + require.False(t, ok) + assert.Equal(t, "", got) +} + +func TestPageGetAttributeEmpty(t *testing.T) { + t.Parallel() + + p := newTestBrowser(t).NewPage(nil) + err := p.SetContent(`Something`, nil) + require.NoError(t, err) + + got, ok, err := p.GetAttribute("#el", "empty", nil) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, "", got) +}