diff --git a/api/element_handle.go b/api/element_handle.go index b9e563426..43c9552fd 100644 --- a/api/element_handle.go +++ b/api/element_handle.go @@ -27,8 +27,8 @@ type ElementHandle interface { IsVisible() bool OwnerFrame() Frame Press(key string, opts goja.Value) - Query(selector string) ElementHandle - QueryAll(selector string) []ElementHandle + Query(selector string) (ElementHandle, error) + QueryAll(selector string) ([]ElementHandle, error) Screenshot(opts goja.Value) goja.ArrayBuffer ScrollIntoViewIfNeeded(opts goja.Value) SelectOption(values goja.Value, opts goja.Value) []string diff --git a/api/frame.go b/api/frame.go index 5f474a7fc..1b02e70f8 100644 --- a/api/frame.go +++ b/api/frame.go @@ -35,8 +35,8 @@ type Frame interface { // Locator creates and returns a new locator for this frame. Locator(selector string, opts goja.Value) Locator Name() string - Query(selector string) ElementHandle - QueryAll(selector string) []ElementHandle + Query(selector string) (ElementHandle, error) + QueryAll(selector string) ([]ElementHandle, error) Page() Page ParentFrame() Frame Press(selector string, key string, opts goja.Value) diff --git a/api/page.go b/api/page.go index af056ddda..38589ceb5 100644 --- a/api/page.go +++ b/api/page.go @@ -51,8 +51,8 @@ type Page interface { Pause() Pdf(opts goja.Value) []byte Press(selector string, key string, opts goja.Value) - Query(selector string) ElementHandle - QueryAll(selector string) []ElementHandle + Query(selector string) (ElementHandle, error) + QueryAll(selector string) ([]ElementHandle, error) Reload(opts goja.Value) Response Route(url goja.Value, handler goja.Callable) Screenshot(opts goja.Value) goja.ArrayBuffer diff --git a/browser/mapping.go b/browser/mapping.go index 2f54e4db0..29693b9c5 100644 --- a/browser/mapping.go +++ b/browser/mapping.go @@ -245,21 +245,25 @@ func mapElementHandle(vu moduleVU, eh api.ElementHandle) mapping { return rt.ToValue(ehm).ToObject(rt) }, } - maps["$"] = func(selector string) *goja.Object { - eh := eh.Query(selector) + maps["$"] = func(selector string) (mapping, error) { + eh, err := eh.Query(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } ehm := mapElementHandle(vu, eh) - return rt.ToValue(ehm).ToObject(rt) + return ehm, nil } - maps["$$"] = func(selector string) *goja.Object { - var ( - mehs []mapping - ehs = eh.QueryAll(selector) - ) + maps["$$"] = func(selector string) ([]mapping, error) { + ehs, err := eh.QueryAll(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + var mehs []mapping for _, eh := range ehs { ehm := mapElementHandle(vu, eh) mehs = append(mehs, ehm) } - return rt.ToValue(mehs).ToObject(rt) + return mehs, nil } jsHandleMap := mapJSHandle(vu, eh) @@ -377,21 +381,25 @@ func mapFrame(vu moduleVU, f api.Frame) mapping { }, "waitForTimeout": f.WaitForTimeout, } - maps["$"] = func(selector string) *goja.Object { - eh := f.Query(selector) + maps["$"] = func(selector string) (mapping, error) { + eh, err := f.Query(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } ehm := mapElementHandle(vu, eh) - return rt.ToValue(ehm).ToObject(rt) + return ehm, nil } - maps["$$"] = func(selector string) *goja.Object { - var ( - mehs []mapping - ehs = f.QueryAll(selector) - ) + maps["$$"] = func(selector string) ([]mapping, error) { + ehs, err := f.QueryAll(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + var mehs []mapping for _, eh := range ehs { ehm := mapElementHandle(vu, eh) mehs = append(mehs, ehm) } - return rt.ToValue(mehs).ToObject(rt) + return mehs, nil } return maps @@ -540,21 +548,25 @@ func mapPage(vu moduleVU, p api.Page) mapping { return rt.ToValue(mws).ToObject(rt) }, } - maps["$"] = func(selector string) *goja.Object { - eh := p.Query(selector) + maps["$"] = func(selector string) (mapping, error) { + eh, err := p.Query(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } ehm := mapElementHandle(vu, eh) - return rt.ToValue(ehm).ToObject(rt) + return ehm, nil } - maps["$$"] = func(selector string) *goja.Object { - var ( - mehs []mapping - ehs = p.QueryAll(selector) - ) + maps["$$"] = func(selector string) ([]mapping, error) { + ehs, err := p.QueryAll(selector) + if err != nil { + return nil, err //nolint:wrapcheck + } + var mehs []mapping for _, eh := range ehs { ehm := mapElementHandle(vu, eh) mehs = append(mehs, ehm) } - return rt.ToValue(mehs).ToObject(rt) + return mehs, nil } return maps diff --git a/common/element_handle.go b/common/element_handle.go index 49bf5a501..9b8635a35 100644 --- a/common/element_handle.go +++ b/common/element_handle.go @@ -1023,7 +1023,7 @@ func (h *ElementHandle) Press(key string, opts goja.Value) { // Query runs "element.querySelector" within the page. If no element matches the selector, // the return value resolves to "null". -func (h *ElementHandle) Query(selector string) api.ElementHandle { +func (h *ElementHandle) Query(selector string) (api.ElementHandle, error) { parsedSelector, err := NewSelector(selector) if err != nil { k6ext.Panic(h.ctx, "parsing selector %q: %w", selector, err) @@ -1039,35 +1039,35 @@ func (h *ElementHandle) Query(selector string) api.ElementHandle { } result, err := h.evalWithScript(h.ctx, opts, fn, parsedSelector) if err != nil { - k6ext.Panic(h.ctx, "querying selector %q: %w", selector, err) + return nil, fmt.Errorf("querying selector %q: %w", selector, err) } if result == nil { - return nil + return nil, fmt.Errorf("querying selector %q", selector) } - - var ( - handle = result.(api.JSHandle) - element = handle.AsElement() - ) - applySlowMo(h.ctx) - if element != nil { - return element + handle, ok := result.(api.JSHandle) + if !ok { + return nil, fmt.Errorf("querying selector %q, wrong type %T", selector, result) } - handle.Dispose() - return nil + element := handle.AsElement() + if element == nil { + handle.Dispose() + return nil, fmt.Errorf("querying selector %q", selector) + } + + return element, nil } // QueryAll queries element subtree for matching elements. // If no element matches the selector, the return value resolves to "null". -func (h *ElementHandle) QueryAll(selector string) []api.ElementHandle { +func (h *ElementHandle) QueryAll(selector string) ([]api.ElementHandle, error) { defer applySlowMo(h.ctx) handles, err := h.queryAll(selector, h.evalWithScript) if err != nil { - k6ext.Panic(h.ctx, "querying all selector %q: %w", selector, err) + return nil, fmt.Errorf("querying all selector %q: %w", selector, err) } - return handles + return handles, nil } func (h *ElementHandle) queryAll(selector string, eval evalFunc) ([]api.ElementHandle, error) { diff --git a/common/frame.go b/common/frame.go index 54f30aeec..e556e17ec 100644 --- a/common/frame.go +++ b/common/frame.go @@ -1304,32 +1304,25 @@ func (f *Frame) Name() string { // Query runs a selector query against the document tree, returning the first matching element or // "null" if no match is found. -func (f *Frame) Query(selector string) api.ElementHandle { +func (f *Frame) Query(selector string) (api.ElementHandle, error) { f.log.Debugf("Frame:Query", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) document, err := f.document() if err != nil { k6ext.Panic(f.ctx, "getting document: %w", err) } - value := document.Query(selector) - if value != nil { - return value - } - return nil + return document.Query(selector) } -func (f *Frame) QueryAll(selector string) []api.ElementHandle { +// QueryAll runs a selector query against the document tree, returning all matching elements. +func (f *Frame) QueryAll(selector string) ([]api.ElementHandle, error) { f.log.Debugf("Frame:QueryAll", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector) document, err := f.document() if err != nil { k6ext.Panic(f.ctx, "getting document: %w", err) } - value := document.QueryAll(selector) - if value != nil { - return value - } - return nil + return document.QueryAll(selector) } // Page returns page that owns frame. diff --git a/common/page.go b/common/page.go index 41100971e..74327dbdc 100644 --- a/common/page.go +++ b/common/page.go @@ -675,13 +675,15 @@ func (p *Page) Press(selector string, key string, opts goja.Value) { p.MainFrame().Press(selector, key, opts) } -func (p *Page) Query(selector string) api.ElementHandle { +// Query returns the first element matching the specified selector. +func (p *Page) Query(selector string) (api.ElementHandle, error) { p.logger.Debugf("Page:Query", "sid:%v selector:%s", p.sessionID(), selector) return p.frameManager.MainFrame().Query(selector) } -func (p *Page) QueryAll(selector string) []api.ElementHandle { +// QueryAll returns all elements matching the specified selector. +func (p *Page) QueryAll(selector string) ([]api.ElementHandle, error) { p.logger.Debugf("Page:QueryAll", "sid:%v selector:%s", p.sessionID(), selector) return p.frameManager.MainFrame().QueryAll(selector) diff --git a/tests/element_handle_test.go b/tests/element_handle_test.go index 40fc7cebd..7e12a1355 100644 --- a/tests/element_handle_test.go +++ b/tests/element_handle_test.go @@ -58,8 +58,8 @@ func TestElementHandleBoundingBoxInvisibleElement(t *testing.T) { p := newTestBrowser(t).NewPage(nil) p.SetContent(`
hello
`, nil) - element := p.Query("div") - + element, err := p.Query("div") + require.NoError(t, err) require.Nil(t, element.BoundingBox()) } @@ -72,7 +72,10 @@ func TestElementHandleBoundingBoxSVG(t *testing.T) { `, nil) - element := p.Query("#therect") + + element, err := p.Query("#therect") + require.NoError(t, err) + bbox := element.BoundingBox() pageFn := `e => { const rect = e.getBoundingClientRect(); @@ -81,7 +84,7 @@ func TestElementHandleBoundingBoxSVG(t *testing.T) { var r api.Rect webBbox := p.Evaluate(tb.toGojaValue(pageFn), tb.toGojaValue(element)) wb, _ := webBbox.(goja.Value) - err := tb.runtime().ExportTo(wb, &r) + err = tb.runtime().ExportTo(wb, &r) require.NoError(t, err) require.EqualValues(t, bbox, &r) @@ -93,8 +96,10 @@ func TestElementHandleClick(t *testing.T) { p.SetContent(htmlInputButton, nil) - button := p.Query("button") - err := button.Click(tb.toGojaValue(struct { + button, err := p.Query("button") + require.NoError(t, err) + + err = button.Click(tb.toGojaValue(struct { NoWaitAfter bool `js:"noWaitAfter"` }{ // FIX: this is just a workaround because navigation is never triggered @@ -116,8 +121,10 @@ func TestElementHandleClickWithNodeRemoved(t *testing.T) { // Remove all nodes p.Evaluate(tb.toGojaValue("() => delete window['Node']")) - button := p.Query("button") - err := button.Click(tb.toGojaValue(struct { + button, err := p.Query("button") + require.NoError(t, err) + + err = button.Click(tb.toGojaValue(struct { NoWaitAfter bool `js:"noWaitAfter"` }{ // FIX: this is just a workaround because navigation is never triggered @@ -135,12 +142,13 @@ func TestElementHandleClickWithDetachedNode(t *testing.T) { p := tb.NewPage(nil) p.SetContent(htmlInputButton, nil) - button := p.Query("button") + button, err := p.Query("button") + require.NoError(t, err) // Detach node to panic when clicked p.Evaluate(tb.toGojaValue("button => button.remove()"), tb.toGojaValue(button)) - err := button.Click(tb.toGojaValue(struct { + err = button.Click(tb.toGojaValue(struct { NoWaitAfter bool `js:"noWaitAfter"` }{ // FIX: this is just a workaround because navigation is never triggered and we'd be waiting for @@ -209,7 +217,9 @@ func TestElementHandleGetAttribute(t *testing.T) { Dark `, nil) - el := p.Query("#dark-mode-toggle-X") + el, err := p.Query("#dark-mode-toggle-X") + require.NoError(t, err) + got := el.GetAttribute("href").String() assert.Equal(t, want, got) } @@ -223,17 +233,23 @@ func TestElementHandleInputValue(t *testing.T) { `, nil) - element := p.Query("input") + element, err := p.Query("input") + require.NoError(t, err) + value := element.InputValue(nil) element.Dispose() assert.Equal(t, value, "hello1", `expected input value "hello1", got %q`, value) - element = p.Query("select") + element, err = p.Query("select") + require.NoError(t, err) + value = element.InputValue(nil) element.Dispose() assert.Equal(t, value, "hello2", `expected input value "hello2", got %q`, value) - element = p.Query("textarea") + element, err = p.Query("textarea") + require.NoError(t, err) + value = element.InputValue(nil) element.Dispose() assert.Equal(t, value, "hello3", `expected input value "hello3", got %q`, value) @@ -243,12 +259,15 @@ func TestElementHandleIsChecked(t *testing.T) { p := newTestBrowser(t).NewPage(nil) p.SetContent(``, nil) - element := p.Query("input") + element, err := p.Query("input") + require.NoError(t, err) + assert.True(t, element.IsChecked(), "expected checkbox to be checked") element.Dispose() p.SetContent(``, nil) - element = p.Query("input") + element, err = p.Query("input") + require.NoError(t, err) assert.False(t, element.IsChecked(), "expected checkbox to be unchecked") element.Dispose() } @@ -268,13 +287,24 @@ func TestElementHandleQueryAll(t *testing.T) { `, nil) t.Run("element_handle", func(t *testing.T) { - assert.Equal(t, wantLiLen, len(p.Query("#aul").QueryAll(query))) + el, err := p.Query("#aul") + require.NoError(t, err) + + els, err := el.QueryAll(query) + require.NoError(t, err) + + assert.Equal(t, wantLiLen, len(els)) }) t.Run("page", func(t *testing.T) { - assert.Equal(t, wantLiLen, len(p.QueryAll(query))) + els, err := p.QueryAll(query) + require.NoError(t, err) + + assert.Equal(t, wantLiLen, len(els)) }) t.Run("frame", func(t *testing.T) { - assert.Equal(t, wantLiLen, len(p.MainFrame().QueryAll(query))) + els, err := p.MainFrame().QueryAll(query) + require.NoError(t, err) + assert.Equal(t, wantLiLen, len(els)) }) } @@ -304,7 +334,9 @@ func TestElementHandleScreenshot(t *testing.T) { } `)) - elem := p.Query("div") + elem, err := p.Query("div") + require.NoError(t, err) + buf := elem.Screenshot(nil) reader := bytes.NewReader(buf.Bytes()) @@ -329,7 +361,9 @@ func TestElementHandleWaitForSelector(t *testing.T) { p := tb.NewPage(nil) p.SetContent(`
`, nil) - root := p.Query(".root") + root, err := p.Query(".root") + require.NoError(t, err) + p.Evaluate(tb.toGojaValue(` () => { setTimeout(() => { @@ -357,7 +391,8 @@ func TestElementHandlePress(t *testing.T) { p.SetContent(``, nil) - el := p.Query("input") + el, err := p.Query("input") + require.NoError(t, err) el.Press("Shift+KeyA", nil) el.Press("KeyB", nil) diff --git a/tests/keyboard_test.go b/tests/keyboard_test.go index 601d021ca..00b1fbed4 100644 --- a/tests/keyboard_test.go +++ b/tests/keyboard_test.go @@ -31,7 +31,8 @@ func TestKeyboardPress(t *testing.T) { kb := p.GetKeyboard() p.SetContent(``, nil) - el := p.Query("input") + el, err := p.Query("input") + require.NoError(t, err) p.Focus("input", nil) kb.Type("Hello World!", nil) @@ -46,7 +47,8 @@ func TestKeyboardPress(t *testing.T) { kb := p.GetKeyboard() p.SetContent(``, nil) - el := p.Query("input") + el, err := p.Query("input") + require.NoError(t, err) p.Focus("input", nil) kb.Press("Shift++", nil) @@ -70,7 +72,8 @@ func TestKeyboardPress(t *testing.T) { kb := p.GetKeyboard() p.SetContent(``, nil) - el := p.Query("input") + el, err := p.Query("input") + require.NoError(t, err) p.Focus("input", nil) kb.Press("Shift+KeyA", nil) @@ -93,7 +96,8 @@ func TestKeyboardPress(t *testing.T) { kb := p.GetKeyboard() p.SetContent(`