Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

De-sobekify the Tap methods and remove panics #1265

Merged
merged 12 commits into from
Apr 18, 2024
74 changes: 49 additions & 25 deletions browser/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,13 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping {
"press": lo.Press,
"type": lo.Type,
"hover": lo.Hover,
"tap": lo.Tap,
"tap": func(opts goja.Value) error {
copts := common.NewFrameTapOptions(lo.DefaultTimeout())
if err := copts.Parse(vu.Context(), opts); err != nil {
return fmt.Errorf("parsing locator tap options: %w", err)
}
return lo.Tap(copts) //nolint:wrapcheck
},
"dispatchEvent": func(typ string, eventInit, opts goja.Value) error {
popts := common.NewFrameDispatchEventOptions(lo.DefaultTimeout())
if err := popts.Parse(vu.Context(), opts); err != nil {
Expand Down Expand Up @@ -298,11 +304,17 @@ func mapElementHandle(vu moduleVU, eh *common.ElementHandle) mapping {
"selectOption": eh.SelectOption,
"selectText": eh.SelectText,
"setInputFiles": eh.SetInputFiles,
"tap": eh.Tap,
"textContent": eh.TextContent,
"type": eh.Type,
"uncheck": eh.Uncheck,
"waitForElementState": eh.WaitForElementState,
"tap": func(opts goja.Value) error {
popts := common.NewElementHandleTapOptions(eh.Timeout())
if err := popts.Parse(vu.Context(), opts); err != nil {
return fmt.Errorf("parsing element tap options: %w", err)
}
return eh.Tap(popts) //nolint:wrapcheck
},
"textContent": eh.TextContent,
"type": eh.Type,
"uncheck": eh.Uncheck,
"waitForElementState": eh.WaitForElementState,
"waitForSelector": func(selector string, opts goja.Value) (mapping, error) {
eh, err := eh.WaitForSelector(selector, opts)
if err != nil {
Expand Down Expand Up @@ -450,12 +462,18 @@ func mapFrame(vu moduleVU, f *common.Frame) mapping {
"selectOption": f.SelectOption,
"setContent": f.SetContent,
"setInputFiles": f.SetInputFiles,
"tap": f.Tap,
"textContent": f.TextContent,
"title": f.Title,
"type": f.Type,
"uncheck": f.Uncheck,
"url": f.URL,
"tap": func(selector string, opts goja.Value) error {
popts := common.NewFrameTapOptions(f.Timeout())
if err := popts.Parse(vu.Context(), opts); err != nil {
return fmt.Errorf("parsing frame tap options: %w", err)
}
return f.Tap(selector, popts) //nolint:wrapcheck
},
"textContent": f.TextContent,
"title": f.Title,
"type": f.Type,
"uncheck": f.Uncheck,
"url": f.URL,
"waitForFunction": func(pageFunc, opts goja.Value, args ...goja.Value) (*goja.Promise, error) {
js, popts, pargs, err := parseWaitForFunctionArgs(
vu.Context(), f.Timeout(), pageFunc, opts, args...,
Expand Down Expand Up @@ -703,19 +721,25 @@ func mapPage(vu moduleVU, p *common.Page) mapping {
"setExtraHTTPHeaders": p.SetExtraHTTPHeaders,
"setInputFiles": p.SetInputFiles,
"setViewportSize": p.SetViewportSize,
"tap": p.Tap,
"textContent": p.TextContent,
"throttleCPU": p.ThrottleCPU,
"throttleNetwork": p.ThrottleNetwork,
"title": p.Title,
"touchscreen": rt.ToValue(p.GetTouchscreen()).ToObject(rt),
"type": p.Type,
"uncheck": p.Uncheck,
"unroute": p.Unroute,
"url": p.URL,
"video": p.Video,
"viewportSize": p.ViewportSize,
"waitForEvent": p.WaitForEvent,
"tap": func(selector string, opts goja.Value) error {
popts := common.NewFrameTapOptions(p.Timeout())
if err := popts.Parse(vu.Context(), opts); err != nil {
return fmt.Errorf("parsing page tap options: %w", err)
}
return p.Tap(selector, popts) //nolint:wrapcheck
},
"textContent": p.TextContent,
"throttleCPU": p.ThrottleCPU,
"throttleNetwork": p.ThrottleNetwork,
"title": p.Title,
"touchscreen": rt.ToValue(p.GetTouchscreen()).ToObject(rt),
"type": p.Type,
"uncheck": p.Uncheck,
"unroute": p.Unroute,
"url": p.URL,
"video": p.Video,
"viewportSize": p.ViewportSize,
"waitForEvent": p.WaitForEvent,
"waitForFunction": func(pageFunc, opts goja.Value, args ...goja.Value) (*goja.Promise, error) {
js, popts, pargs, err := parseWaitForFunctionArgs(
vu.Context(), p.Timeout(), pageFunc, opts, args...,
Expand Down
10 changes: 5 additions & 5 deletions browser/mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ type pageAPI interface {
SetExtraHTTPHeaders(headers map[string]string)
SetInputFiles(selector string, files goja.Value, opts goja.Value)
SetViewportSize(viewportSize goja.Value)
Tap(selector string, opts goja.Value)
Tap(selector string, opts goja.Value) error
TextContent(selector string, opts goja.Value) string
ThrottleCPU(common.CPUProfile) error
ThrottleNetwork(common.NetworkProfile) error
Expand Down Expand Up @@ -413,7 +413,7 @@ type frameAPI interface {
SelectOption(selector string, values goja.Value, opts goja.Value) []string
SetContent(html string, opts goja.Value)
SetInputFiles(selector string, files goja.Value, opts goja.Value)
Tap(selector string, opts goja.Value)
Tap(selector string, opts goja.Value) error
TextContent(selector string, opts goja.Value) string
Title() string
Type(selector string, text string, opts goja.Value)
Expand Down Expand Up @@ -458,7 +458,7 @@ type elementHandleAPI interface {
SelectOption(values goja.Value, opts goja.Value) []string
SelectText(opts goja.Value)
SetInputFiles(files goja.Value, opts goja.Value)
Tap(opts goja.Value)
Tap(opts goja.Value) error
TextContent() string
Type(text string, opts goja.Value)
Uncheck(opts goja.Value)
Expand Down Expand Up @@ -533,7 +533,7 @@ type locatorAPI interface {
Press(key string, opts goja.Value)
Type(text string, opts goja.Value)
Hover(opts goja.Value)
Tap(opts goja.Value)
Tap(opts goja.Value) error
DispatchEvent(typ string, eventInit, opts goja.Value)
WaitFor(opts goja.Value)
}
Expand All @@ -555,7 +555,7 @@ type keyboardAPI interface { //nolint: unused
// mapping is not tested using this interface. We use the concrete type
// without testing its exported methods.
type touchscreenAPI interface { //nolint: unused
Tap(x float64, y float64)
Tap(x float64, y float64) error
}

// mouseAPI is the interface of a mouse input device.
Expand Down
30 changes: 14 additions & 16 deletions common/element_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,10 +589,6 @@ func (h *ElementHandle) selectText(apiCtx context.Context) error {
return nil
}

func (h *ElementHandle) tap(apiCtx context.Context, p *Position) error {
return h.frame.page.Touchscreen.tap(p.X, p.Y)
}

func (h *ElementHandle) textContent(apiCtx context.Context) (any, error) {
js := `
(element) => {
Expand Down Expand Up @@ -1294,22 +1290,24 @@ func (h *ElementHandle) setInputFiles(apiCtx context.Context, payload []*File) e
return nil
}

func (h *ElementHandle) Tap(opts goja.Value) {
parsedOpts := NewElementHandleTapOptions(h.defaultTimeout())
err := parsedOpts.Parse(h.ctx, opts)
if err != nil {
k6ext.Panic(h.ctx, "parsing tap options: %w", err)
}

fn := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) {
// Tap scrolls element into view and taps in the center of the element.
func (h *ElementHandle) Tap(opts *ElementHandleTapOptions) error {
tap := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) {
return nil, handle.tap(apiCtx, p)
}
pointerFn := h.newPointerAction(fn, &parsedOpts.ElementHandleBasePointerOptions)
_, err = call(h.ctx, pointerFn, parsedOpts.Timeout)
if err != nil {
k6ext.Panic(h.ctx, "tapping element: %w", err)
tapAction := h.newPointerAction(tap, &opts.ElementHandleBasePointerOptions)

if _, err := call(h.ctx, tapAction, opts.Timeout); err != nil {
return fmt.Errorf("tapping element: %w", err)
}

applySlowMo(h.ctx)

return nil
}

func (h *ElementHandle) tap(_ context.Context, p *Position) error {
return h.frame.page.Touchscreen.tap(p.X, p.Y)
}

func (h *ElementHandle) TextContent() string {
Expand Down
38 changes: 18 additions & 20 deletions common/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -1549,44 +1549,42 @@ func (f *Frame) SetInputFiles(selector string, files goja.Value, opts goja.Value
}

// Tap the first element that matches the selector.
func (f *Frame) Tap(selector string, opts goja.Value) {
func (f *Frame) Tap(selector string, opts *FrameTapOptions) error {
f.log.Debugf("Frame:Tap", "fid:%s furl:%q sel:%q", f.ID(), f.URL(), selector)

popts := NewFrameTapOptions(f.defaultTimeout())
if err := popts.Parse(f.ctx, opts); err != nil {
k6ext.Panic(f.ctx, "parsing tap options: %w", err)
}
if err := f.tap(selector, popts); err != nil {
k6ext.Panic(f.ctx, "tapping on %q: %w", selector, err)
if err := f.tap(selector, opts); err != nil {
return fmt.Errorf("tapping on %q: %w", selector, err)
}

applySlowMo(f.ctx)

return nil
}

func (f *Frame) setInputFiles(selector string, files *Files, opts *FrameSetInputFilesOptions) error {
setInputFiles := func(apiCtx context.Context, handle *ElementHandle) (any, error) {
return nil, handle.setInputFiles(apiCtx, files.Payload)
func (f *Frame) tap(selector string, opts *FrameTapOptions) error {
tap := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) {
return nil, handle.tap(apiCtx, p)
}
act := f.newAction(
selector, DOMElementStateAttached, opts.Strict,
setInputFiles, []string{},
opts.Force, opts.NoWaitAfter, opts.Timeout,
act := f.newPointerAction(
selector, DOMElementStateAttached, opts.Strict, tap, &opts.ElementHandleBasePointerOptions,
)

if _, err := call(f.ctx, act, opts.Timeout); err != nil {
return errorFromDOMError(err)
}

return nil
}

func (f *Frame) tap(selector string, opts *FrameTapOptions) error {
tap := func(apiCtx context.Context, handle *ElementHandle, p *Position) (any, error) {
return nil, handle.tap(apiCtx, p)
func (f *Frame) setInputFiles(selector string, files *Files, opts *FrameSetInputFilesOptions) error {
setInputFiles := func(apiCtx context.Context, handle *ElementHandle) (any, error) {
return nil, handle.setInputFiles(apiCtx, files.Payload)
}
act := f.newPointerAction(
selector, DOMElementStateAttached, opts.Strict, tap, &opts.ElementHandleBasePointerOptions,
act := f.newAction(
selector, DOMElementStateAttached, opts.Strict,
setInputFiles, []string{},
opts.Force, opts.NoWaitAfter, opts.Timeout,
)

if _, err := call(f.ctx, act, opts.Timeout); err != nil {
return errorFromDOMError(err)
}
Expand Down
23 changes: 7 additions & 16 deletions common/locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,26 +561,17 @@ func (l *Locator) hover(opts *FrameHoverOptions) error {
}

// Tap the element found that matches the locator's selector with strict mode on.
func (l *Locator) Tap(opts goja.Value) {
func (l *Locator) Tap(opts *FrameTapOptions) error {
l.log.Debugf("Locator:Tap", "fid:%s furl:%q sel:%q opts:%+v", l.frame.ID(), l.frame.URL(), l.selector, opts)

var err error
defer func() { panicOrSlowMo(l.ctx, err) }()

copts := NewFrameTapOptions(l.frame.defaultTimeout())
if err = copts.Parse(l.ctx, opts); err != nil {
err = fmt.Errorf("parsing tap options: %w", err)
return
}
if err = l.tap(copts); err != nil {
err = fmt.Errorf("tapping on %q: %w", l.selector, err)
return
opts.Strict = true
if err := l.frame.tap(l.selector, opts); err != nil {
return fmt.Errorf("tapping on %q: %w", l.selector, err)
}
}

func (l *Locator) tap(opts *FrameTapOptions) error {
opts.Strict = true
return l.frame.tap(l.selector, opts)
applySlowMo(l.ctx)

return nil
}

// DispatchEvent dispatches an event for the element matching the
Expand Down
5 changes: 3 additions & 2 deletions common/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -1190,10 +1190,11 @@ func (p *Page) SetViewportSize(viewportSize goja.Value) {
applySlowMo(p.ctx)
}

func (p *Page) Tap(selector string, opts goja.Value) {
// Tap will tap the element matching the provided selector.
func (p *Page) Tap(selector string, opts *FrameTapOptions) error {
p.logger.Debugf("Page:SetViewportSize", "sid:%v selector:%s", p.sessionID(), selector)

p.MainFrame().Tap(selector, opts)
return p.MainFrame().Tap(selector, opts)
}

func (p *Page) TextContent(selector string, opts goja.Value) string {
Expand Down
42 changes: 26 additions & 16 deletions common/touchscreen.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package common

import (
"context"

"github.com/grafana/xk6-browser/k6ext"
"fmt"

"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/input"
Expand All @@ -27,23 +26,34 @@ func NewTouchscreen(ctx context.Context, s session, k *Keyboard) *Touchscreen {
}
}

func (t *Touchscreen) tap(x float64, y float64) error {
action := input.DispatchTouchEvent(input.TouchStart, []*input.TouchPoint{{X: x, Y: y}}).
WithModifiers(input.Modifier(t.keyboard.modifiers))
if err := action.Do(cdp.WithExecutor(t.ctx, t.session)); err != nil {
return err
}
action = input.DispatchTouchEvent(input.TouchEnd, []*input.TouchPoint{}).
WithModifiers(input.Modifier(t.keyboard.modifiers))
if err := action.Do(cdp.WithExecutor(t.ctx, t.session)); err != nil {
return err
// Tap dispatches a tap start and tap end event.
func (t *Touchscreen) Tap(x float64, y float64) error {
if err := t.tap(x, y); err != nil {
return fmt.Errorf("tapping: %w", err)
}
return nil
}

// Tap dispatches a tap start and tap end event.
func (t *Touchscreen) Tap(x float64, y float64) {
if err := t.tap(x, y); err != nil {
k6ext.Panic(t.ctx, "tapping: %w", err)
func (t *Touchscreen) tap(x float64, y float64) error {
touchStart := input.DispatchTouchEvent(
input.TouchStart,
[]*input.TouchPoint{{X: x, Y: y}},
).WithModifiers(
input.Modifier(t.keyboard.modifiers),
)
if err := touchStart.Do(cdp.WithExecutor(t.ctx, t.session)); err != nil {
return fmt.Errorf("touch start: %w", err)
}

touchEnd := input.DispatchTouchEvent(
input.TouchEnd,
[]*input.TouchPoint{},
).WithModifiers(
input.Modifier(t.keyboard.modifiers),
)
if err := touchEnd.Do(cdp.WithExecutor(t.ctx, t.session)); err != nil {
return fmt.Errorf("touch end: %w", err)
}

return nil
}
13 changes: 10 additions & 3 deletions tests/locator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,15 @@ func TestLocator(t *testing.T) {
},
},
{
"Tap", func(tb *testBrowser, p *common.Page) {
"Tap", func(_ *testBrowser, p *common.Page) {
result := func() bool {
v := p.Evaluate(`() => window.result`)
return asBool(t, v)
}
require.False(t, result(), "should not be tapped first")
p.Locator("#inputText", nil).Tap(nil)
opts := common.NewFrameTapOptions(common.DefaultTimeout)
err := p.Locator("#inputText", nil).Tap(opts)
require.NoError(t, err)
require.True(t, result(), "should be tapped")
},
},
Expand Down Expand Up @@ -332,7 +334,12 @@ func TestLocator(t *testing.T) {
"SelectOption", func(l *common.Locator, tb *testBrowser) { l.SelectOption(tb.toGojaValue(""), timeout(tb)) },
},
{
"Tap", func(l *common.Locator, tb *testBrowser) { l.Tap(timeout(tb)) },
"Tap", func(l *common.Locator, _ *testBrowser) {
if err := l.Tap(common.NewFrameTapOptions(100 * time.Millisecond)); err != nil {
// TODO: remove panic and update tests when all locator methods return error.
panic(err)
}
},
},
{
"Type", func(l *common.Locator, tb *testBrowser) { l.Type("a", timeout(tb)) },
Expand Down
Loading