From eb523f01ccd42a4157ca64563308f648e8b9ff80 Mon Sep 17 00:00:00 2001 From: Tim Voronov Date: Sat, 23 Feb 2019 17:52:01 -0500 Subject: [PATCH] Feature/#221 mouse events (#237) * Initial work * Added MoveMouseByXY and ScrollByXY * Fixed liniting issues --- pkg/drivers/cdp/document.go | 24 ++- pkg/drivers/cdp/eval/eval.go | 5 + pkg/drivers/http/document.go | 16 +- pkg/drivers/value.go | 8 +- pkg/runtime/values/helpers.go | 41 ++++- pkg/runtime/values/helpers_test.go | 238 +++++++++++++++++++++++++++++ pkg/runtime/values/object.go | 6 + pkg/stdlib/html/hover.go | 42 +++-- pkg/stdlib/html/lib.go | 5 +- pkg/stdlib/html/mouse_xy.go | 56 +++++++ pkg/stdlib/html/scroll_xy.go | 56 +++++++ 11 files changed, 479 insertions(+), 18 deletions(-) create mode 100644 pkg/stdlib/html/mouse_xy.go create mode 100644 pkg/stdlib/html/scroll_xy.go diff --git a/pkg/drivers/cdp/document.go b/pkg/drivers/cdp/document.go index 8ab6b1a3..e2b989b3 100644 --- a/pkg/drivers/cdp/document.go +++ b/pkg/drivers/cdp/document.go @@ -483,7 +483,7 @@ func (doc *HTMLDocument) SelectBySelector(ctx context.Context, selector values.S return nil, core.TypeError(types.Array, res.Type()) } -func (doc *HTMLDocument) HoverBySelector(ctx context.Context, selector values.String) error { +func (doc *HTMLDocument) MoveMouseBySelector(ctx context.Context, selector values.String) error { err := doc.ScrollBySelector(ctx, selector) if err != nil { @@ -519,6 +519,13 @@ func (doc *HTMLDocument) HoverBySelector(ctx context.Context, selector values.St ) } +func (doc *HTMLDocument) MoveMouseByXY(ctx context.Context, x, y values.Float) error { + return doc.client.Input.DispatchMouseEvent( + ctx, + input.NewDispatchMouseEventArgs("mouseMoved", float64(x), float64(y)), + ) +} + func (doc *HTMLDocument) WaitForSelector(ctx context.Context, selector values.String) error { task := events.NewEvalWaitTask( doc.client, @@ -863,6 +870,21 @@ func (doc *HTMLDocument) ScrollBySelector(ctx context.Context, selector values.S return err } +func (doc *HTMLDocument) ScrollByXY(ctx context.Context, x, y values.Float) error { + _, err := eval.Eval(ctx, doc.client, fmt.Sprintf(` + window.scrollBy({ + top: %s, + left: %s, + behavior: 'instant' + }); + `, + eval.ParamFloat(float64(x)), + eval.ParamFloat(float64(y)), + ), false, false) + + return err +} + func (doc *HTMLDocument) handlePageLoad(ctx context.Context, _ interface{}) { doc.Lock() defer doc.Unlock() diff --git a/pkg/drivers/cdp/eval/eval.go b/pkg/drivers/cdp/eval/eval.go index 7a35c259..f9dd7959 100644 --- a/pkg/drivers/cdp/eval/eval.go +++ b/pkg/drivers/cdp/eval/eval.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" "github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/values" @@ -19,6 +20,10 @@ func ParamString(param string) string { return "`" + param + "`" } +func ParamFloat(param float64) string { + return strconv.FormatFloat(param, 'f', 6, 64) +} + func Eval(ctx context.Context, client *cdp.Client, exp string, ret bool, async bool) (core.Value, error) { args := runtime. NewEvaluateArgs(PrepareEval(exp)). diff --git a/pkg/drivers/http/document.go b/pkg/drivers/http/document.go index 2af39b6c..0cbc75b9 100644 --- a/pkg/drivers/http/document.go +++ b/pkg/drivers/http/document.go @@ -189,10 +189,6 @@ func (doc *HTMLDocument) SelectBySelector(_ context.Context, _ values.String, _ return nil, core.ErrNotSupported } -func (doc *HTMLDocument) HoverBySelector(_ context.Context, _ values.String) error { - return core.ErrNotSupported -} - func (doc *HTMLDocument) PrintToPDF(_ context.Context, _ drivers.PDFParams) (values.Binary, error) { return nil, core.ErrNotSupported } @@ -213,6 +209,18 @@ func (doc *HTMLDocument) ScrollBySelector(_ context.Context, _ values.String) er return core.ErrNotSupported } +func (doc *HTMLDocument) ScrollByXY(_ context.Context, _, _ values.Float) error { + return core.ErrNotSupported +} + +func (doc *HTMLDocument) MoveMouseBySelector(_ context.Context, _ values.String) error { + return core.ErrNotSupported +} + +func (doc *HTMLDocument) MoveMouseByXY(_ context.Context, _, _ values.Float) error { + return core.ErrNotSupported +} + func (doc *HTMLDocument) WaitForNavigation(_ context.Context) error { return core.ErrNotSupported } diff --git a/pkg/drivers/value.go b/pkg/drivers/value.go index a23bf400..e7fce79a 100644 --- a/pkg/drivers/value.go +++ b/pkg/drivers/value.go @@ -102,8 +102,6 @@ type ( SelectBySelector(ctx context.Context, selector values.String, value *values.Array) (*values.Array, error) - HoverBySelector(ctx context.Context, selector values.String) error - PrintToPDF(ctx context.Context, params PDFParams) (values.Binary, error) CaptureScreenshot(ctx context.Context, params ScreenshotParams) (values.Binary, error) @@ -114,6 +112,12 @@ type ( ScrollBySelector(ctx context.Context, selector values.String) error + ScrollByXY(ctx context.Context, x, y values.Float) error + + MoveMouseByXY(ctx context.Context, x, y values.Float) error + + MoveMouseBySelector(ctx context.Context, selector values.String) error + WaitForNavigation(ctx context.Context) error WaitForSelector(ctx context.Context, selector values.String) error diff --git a/pkg/runtime/values/helpers.go b/pkg/runtime/values/helpers.go index 50ee38bf..645e2205 100644 --- a/pkg/runtime/values/helpers.go +++ b/pkg/runtime/values/helpers.go @@ -7,6 +7,7 @@ import ( "hash/fnv" "reflect" "sort" + "strconv" "time" "github.com/MontFerret/ferret/pkg/runtime/core" @@ -257,6 +258,44 @@ func ToBoolean(input core.Value) core.Value { } } +func ToFloat(input core.Value) (Float, error) { + switch val := input.(type) { + case Float: + return val, nil + case Int: + return Float(val), nil + case String: + i, err := strconv.ParseFloat(string(val), 64) + + if err != nil { + return ZeroFloat, err + } + + return Float(i), nil + default: + return ZeroFloat, core.TypeError(input.Type(), types.Int, types.Float, types.String) + } +} + +func ToInt(input core.Value) (Int, error) { + switch val := input.(type) { + case Int: + return val, nil + case Float: + return Int(val), nil + case String: + i, err := strconv.ParseInt(string(val), 10, 64) + + if err != nil { + return ZeroInt, err + } + + return Int(i), nil + default: + return ZeroInt, core.TypeError(input.Type(), types.Int, types.Float, types.String) + } +} + func ToArray(ctx context.Context, input core.Value) (core.Value, error) { switch value := input.(type) { case Boolean, @@ -277,7 +316,7 @@ func ToArray(ctx context.Context, input core.Value) (core.Value, error) { return true }) - return value, nil + return arr, nil case core.Iterable: iterator, err := value.Iterate(ctx) diff --git a/pkg/runtime/values/helpers_test.go b/pkg/runtime/values/helpers_test.go index 44d942ff..4a5279ab 100644 --- a/pkg/runtime/values/helpers_test.go +++ b/pkg/runtime/values/helpers_test.go @@ -155,5 +155,243 @@ func TestHelpers(t *testing.T) { So(qaz, ShouldEqual, values.NewString("foobar")) }) }) + + Convey("ToBoolean", func() { + Convey("Should convert values", func() { + inputs := [][]core.Value{ + { + values.None, + values.False, + }, + { + values.True, + values.True, + }, + { + values.False, + values.False, + }, + { + values.NewInt(1), + values.True, + }, + { + values.NewInt(0), + values.False, + }, + { + values.NewFloat(1), + values.True, + }, + { + values.NewFloat(0), + values.False, + }, + { + values.NewString("Foo"), + values.True, + }, + { + values.EmptyString, + values.False, + }, + { + values.NewCurrentDateTime(), + values.True, + }, + { + values.NewArray(1), + values.True, + }, + { + values.NewObject(), + values.True, + }, + { + values.NewBinary([]byte("")), + values.True, + }, + } + + for _, pair := range inputs { + actual := values.ToBoolean(pair[0]) + expected := pair[1] + + So(actual, ShouldEqual, expected) + } + }) + }) + + Convey("ToFloat", func() { + Convey("Should convert Int", func() { + input := values.NewInt(100) + output, err := values.ToFloat(input) + + So(err, ShouldBeNil) + So(output, ShouldEqual, values.NewFloat(100)) + }) + + Convey("Should convert Float", func() { + input := values.NewFloat(100) + output, err := values.ToFloat(input) + + So(err, ShouldBeNil) + So(output, ShouldEqual, values.NewFloat(100)) + }) + + Convey("Should convert String", func() { + input := values.NewString("100.1") + output, err := values.ToFloat(input) + + So(err, ShouldBeNil) + So(output, ShouldEqual, values.NewFloat(100.1)) + }) + + Convey("Should NOT convert other types", func() { + inputs := []core.Value{ + values.NewBoolean(true), + values.NewCurrentDateTime(), + values.NewArray(1), + values.NewObject(), + values.NewBinary([]byte("")), + } + + for _, input := range inputs { + _, err := values.ToFloat(input) + + So(err, ShouldNotBeNil) + } + }) + }) + + Convey("ToInt", func() { + Convey("Should convert Int", func() { + input := values.NewInt(100) + output, err := values.ToInt(input) + + So(err, ShouldBeNil) + So(output, ShouldEqual, values.NewInt(100)) + }) + + Convey("Should convert Float", func() { + input := values.NewFloat(100.1) + output, err := values.ToInt(input) + + So(err, ShouldBeNil) + So(output, ShouldEqual, values.NewInt(100)) + }) + + Convey("Should convert String", func() { + input := values.NewString("100") + output, err := values.ToInt(input) + + So(err, ShouldBeNil) + So(output, ShouldEqual, values.NewInt(100)) + }) + + Convey("Should NOT convert other types", func() { + inputs := []core.Value{ + values.NewBoolean(true), + values.NewCurrentDateTime(), + values.NewArray(1), + values.NewObject(), + values.NewBinary([]byte("")), + } + + for _, input := range inputs { + _, err := values.ToInt(input) + + So(err, ShouldNotBeNil) + } + }) + }) + + Convey("ToArray", func() { + Convey("Should convert primitives", func() { + dt := values.NewCurrentDateTime() + + inputs := [][]core.Value{ + { + values.None, + values.NewArray(0), + }, + { + values.True, + values.NewArrayWith(values.True), + }, + { + values.NewInt(1), + values.NewArrayWith(values.NewInt(1)), + }, + { + values.NewFloat(1), + values.NewArrayWith(values.NewFloat(1)), + }, + { + values.NewString("foo"), + values.NewArrayWith(values.NewString("foo")), + }, + { + dt, + values.NewArrayWith(dt), + }, + } + + for _, pairs := range inputs { + actual, err := values.ToArray(context.Background(), pairs[0]) + expected := pairs[1] + + So(err, ShouldBeNil) + So(actual.Compare(expected), ShouldEqual, 0) + } + }) + + Convey("Should create a copy of a given array", func() { + vals := []core.Value{ + values.NewInt(1), + values.NewInt(2), + values.NewInt(3), + values.NewInt(4), + values.NewArray(10), + values.NewObject(), + } + + input := values.NewArrayWith(vals...) + output, err := values.ToArray(context.Background(), input) + + So(err, ShouldBeNil) + + arr := output.(*values.Array) + + So(input == arr, ShouldBeFalse) + So(arr.Length() == input.Length(), ShouldBeTrue) + + for idx := range vals { + expected := input.Get(values.NewInt(idx)) + actual := arr.Get(values.NewInt(idx)) + + // same ref + So(actual == expected, ShouldBeTrue) + So(actual.Compare(expected), ShouldEqual, 0) + } + }) + + Convey("Should convert object to an array", func() { + input := values.NewObjectWith( + values.NewObjectProperty("foo", values.NewString("bar")), + values.NewObjectProperty("baz", values.NewInt(1)), + values.NewObjectProperty("qaz", values.NewObject()), + ) + + output, err := values.ToArray(context.Background(), input) + + So(err, ShouldBeNil) + + arr := output.(*values.Array).Sort() + + So(arr.String(), ShouldEqual, "[1,\"bar\",{}]") + So(arr.Get(values.NewInt(2)) == input.MustGet("qaz"), ShouldBeTrue) + }) + }) }) } diff --git a/pkg/runtime/values/object.go b/pkg/runtime/values/object.go index af8ef86d..dae45350 100644 --- a/pkg/runtime/values/object.go +++ b/pkg/runtime/values/object.go @@ -196,6 +196,12 @@ func (t *Object) ForEach(predicate ObjectPredicate) { } } +func (t *Object) MustGet(key String) core.Value { + val, _ := t.Get(key) + + return val +} + func (t *Object) Get(key String) (core.Value, Boolean) { val, found := t.value[string(key)] diff --git a/pkg/stdlib/html/hover.go b/pkg/stdlib/html/hover.go index 4f72abf3..7db3fea8 100644 --- a/pkg/stdlib/html/hover.go +++ b/pkg/stdlib/html/hover.go @@ -27,22 +27,46 @@ func Hover(ctx context.Context, args ...core.Value) (core.Value, error) { return values.None, err } - if len(args) == 2 { + selector := values.EmptyString + + if len(args) > 1 { err = core.ValidateType(args[1], types.String) if err != nil { return values.None, err } - // Document with a selector - doc := args[0].(drivers.HTMLDocument) - selector := args[1].(values.String) - - return values.None, doc.HoverBySelector(ctx, selector) + selector = args[1].(values.String) } - // Element - el := args[0].(drivers.HTMLElement) + switch n := args[0].(type) { + case drivers.HTMLDocument: + if selector == values.EmptyString { + return values.None, core.Error(core.ErrMissedArgument, "selector") + } - return values.None, el.Hover(ctx) + return values.None, n.MoveMouseBySelector(ctx, selector) + case drivers.HTMLElement: + if selector == values.EmptyString { + return values.None, n.Hover(ctx) + } + + found := n.QuerySelector(ctx, selector) + + if found == values.None { + return values.None, core.Errorf(core.ErrNotFound, "element by selector %s", selector) + } + + el, ok := found.(drivers.HTMLElement) + + if !ok { + return values.None, core.Errorf(core.ErrNotFound, "element by selector %s", selector) + } + + defer el.Close() + + return values.None, el.Hover(ctx) + default: + return values.None, core.TypeError(n.Type(), drivers.HTMLDocumentType, drivers.HTMLElementType) + } } diff --git a/pkg/stdlib/html/lib.go b/pkg/stdlib/html/lib.go index 08808bb8..a82bcbc1 100644 --- a/pkg/stdlib/html/lib.go +++ b/pkg/stdlib/html/lib.go @@ -2,11 +2,12 @@ package html import ( "context" + "time" + "github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values/types" - "time" ) const defaultTimeout = 5000 @@ -27,12 +28,14 @@ func NewLib() map[string]core.Function { "INNER_TEXT": InnerText, "INNER_TEXT_ALL": InnerTextAll, "INPUT": Input, + "MOUSE": MouseMoveXY, "NAVIGATE": Navigate, "NAVIGATE_BACK": NavigateBack, "NAVIGATE_FORWARD": NavigateForward, "PAGINATION": Pagination, "PDF": PDF, "SCREENSHOT": Screenshot, + "SCROLL": ScrollXY, "SCROLL_BOTTOM": ScrollBottom, "SCROLL_ELEMENT": ScrollInto, "SCROLL_TOP": ScrollTop, diff --git a/pkg/stdlib/html/mouse_xy.go b/pkg/stdlib/html/mouse_xy.go new file mode 100644 index 00000000..a16e31cc --- /dev/null +++ b/pkg/stdlib/html/mouse_xy.go @@ -0,0 +1,56 @@ +package html + +import ( + "context" + + "github.com/MontFerret/ferret/pkg/drivers" + "github.com/MontFerret/ferret/pkg/runtime/core" + "github.com/MontFerret/ferret/pkg/runtime/values" + "github.com/MontFerret/ferret/pkg/runtime/values/types" +) + +// MouseMoveXY moves mouse by given coordinates. +// @param doc (HTMLDocument) - HTML document. +// @param x (Int|Float) - X coordinate. +// @param y (Int|Float) - Y coordinate. +func MouseMoveXY(ctx context.Context, args ...core.Value) (core.Value, error) { + err := core.ValidateArgs(args, 3, 3) + + if err != nil { + return values.None, err + } + + err = core.ValidateType(args[0], drivers.HTMLDocumentType) + + if err != nil { + return values.None, err + } + + err = core.ValidateType(args[1], types.Int, types.Float) + + if err != nil { + return values.None, err + } + + err = core.ValidateType(args[2], types.Int, types.Float) + + if err != nil { + return values.None, err + } + + x, err := values.ToFloat(args[0]) + + if err != nil { + return values.None, err + } + + y, err := values.ToFloat(args[1]) + + if err != nil { + return values.None, err + } + + doc := args[0].(drivers.HTMLDocument) + + return values.None, doc.MoveMouseByXY(ctx, x, y) +} diff --git a/pkg/stdlib/html/scroll_xy.go b/pkg/stdlib/html/scroll_xy.go new file mode 100644 index 00000000..6af7c9db --- /dev/null +++ b/pkg/stdlib/html/scroll_xy.go @@ -0,0 +1,56 @@ +package html + +import ( + "context" + "github.com/MontFerret/ferret/pkg/drivers" + + "github.com/MontFerret/ferret/pkg/runtime/core" + "github.com/MontFerret/ferret/pkg/runtime/values" + "github.com/MontFerret/ferret/pkg/runtime/values/types" +) + +// ScrollXY scrolls by given coordinates. +// @param doc (HTMLDocument) - HTML document. +// @param x (Int|Float) - X coordinate. +// @param y (Int|Float) - Y coordinate. +func ScrollXY(ctx context.Context, args ...core.Value) (core.Value, error) { + err := core.ValidateArgs(args, 3, 3) + + if err != nil { + return values.None, err + } + + err = core.ValidateType(args[0], drivers.HTMLDocumentType) + + if err != nil { + return values.None, err + } + + err = core.ValidateType(args[1], types.Int, types.Float) + + if err != nil { + return values.None, err + } + + err = core.ValidateType(args[2], types.Int, types.Float) + + if err != nil { + return values.None, err + } + + x, err := values.ToFloat(args[0]) + + if err != nil { + return values.None, err + } + + y, err := values.ToFloat(args[1]) + + if err != nil { + return values.None, err + } + + doc := args[0].(drivers.HTMLDocument) + + return values.None, doc.ScrollByXY(ctx, x, y) +}