1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-11-27 22:08:15 +02:00

Added possibility to dispatch events on node

This commit is contained in:
Tim Voronov
2018-09-25 11:43:58 -04:00
parent 2a1bac6650
commit 64d0f585b4
15 changed files with 531 additions and 156 deletions

28
Gopkg.lock generated
View File

@@ -66,7 +66,7 @@
version = "v4.2.1" version = "v4.2.1"
[[projects]] [[projects]]
digest = "1:383ac09778833e583aa5f74cf52a71367c72ba43a7de1460e8ff95182ad93b3b" digest = "1:7ffdd69928c5153fc351132aa80bbcc18a8f8122de1ba592cf42dccb65732361"
name = "github.com/mafredri/cdp" name = "github.com/mafredri/cdp"
packages = [ packages = [
".", ".",
@@ -115,6 +115,7 @@
"protocol/tethering", "protocol/tethering",
"protocol/tracing", "protocol/tracing",
"rpcc", "rpcc",
"session",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "75b0ecc5efcff27ac756a33ec71f0db75dc3d21c" revision = "75b0ecc5efcff27ac756a33ec71f0db75dc3d21c"
@@ -180,25 +181,6 @@
pruneopts = "UT" pruneopts = "UT"
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]]
digest = "1:45ed1634373de798161249f031804bac6f5ec7707bbae94047ecd8e69875936c"
name = "golang.org/x/text"
packages = [
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"search",
"transform",
"unicode/cldr",
"unicode/norm",
]
pruneopts = "UT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
@@ -210,13 +192,17 @@
"github.com/mafredri/cdp", "github.com/mafredri/cdp",
"github.com/mafredri/cdp/devtool", "github.com/mafredri/cdp/devtool",
"github.com/mafredri/cdp/protocol/dom", "github.com/mafredri/cdp/protocol/dom",
"github.com/mafredri/cdp/protocol/emulation",
"github.com/mafredri/cdp/protocol/input",
"github.com/mafredri/cdp/protocol/page",
"github.com/mafredri/cdp/protocol/runtime",
"github.com/mafredri/cdp/rpcc", "github.com/mafredri/cdp/rpcc",
"github.com/mafredri/cdp/session",
"github.com/pkg/errors", "github.com/pkg/errors",
"github.com/sethgrid/pester", "github.com/sethgrid/pester",
"github.com/smartystreets/goconvey/convey", "github.com/smartystreets/goconvey/convey",
"golang.org/x/net/html", "golang.org/x/net/html",
"golang.org/x/sync/errgroup", "golang.org/x/sync/errgroup",
"golang.org/x/text/search",
] ]
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

9
docs/examples/click.fql Normal file
View File

@@ -0,0 +1,9 @@
LET doc = DOCUMENT("https://github.com/", true)
LET btn = ELEMENT(doc, ".HeaderMenu a")
LET clicked = CLICK(btn)
WAIT_ELEMENT(doc, '.IconNav', 5000)
WAIT(5000)
FOR el IN ELEMENTS(doc, '.IconNav a')
RETURN el.innerText

View File

@@ -4,8 +4,6 @@ WAIT_ELEMENT(doc, '.chartTrack__details', 5000)
LET tracks = ELEMENTS(doc, '.chartTrack__details') LET tracks = ELEMENTS(doc, '.chartTrack__details')
// LOG("found", LENGTH(tracks), "tracks")
FOR track IN tracks FOR track IN tracks
LET username = ELEMENT(track, '.chartTrack__username') LET username = ELEMENT(track, '.chartTrack__username')
LET title = ELEMENT(track, '.chartTrack__title') LET title = ELEMENT(track, '.chartTrack__title')

View File

@@ -1,8 +0,0 @@
LET reddit = DOCUMENT("https://www.reddit.com/")
LET urls = ELEMENTS(reddit, 'a[data-click-id="body"]')
FOR url IN urls
LET subreddit = DOCUMENT("https://www.reddit.com" + TRIM(url.attributes.href))
LET post = ELEMENT(subreddit, 'div[data-test-id="post-content"] > div:nth-child(2) > div')
RETURN { title: post.children[0].innerText }

View File

@@ -0,0 +1,56 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/browser"
)
/*
*
*/
func Click(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 2)
if err != nil {
return values.False, err
}
// CLICK(el)
if len(args) == 1 {
arg1 := args[0]
err := core.ValidateType(arg1, core.HtmlElementType)
if err != nil {
return values.False, err
}
el, ok := arg1.(*browser.HtmlElement)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic element")
}
return el.Click()
} else {
// CLICK(doc, selector)
arg1 := args[0]
selector := args[1].String()
err = core.ValidateType(arg1, core.HtmlDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg1.(*browser.HtmlDocument)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return doc.ClickBySelector(values.NewString(selector))
}
}

View File

@@ -0,0 +1,103 @@
package browser
import (
"context"
"fmt"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/mafredri/cdp/protocol/page"
)
type (
EventHandler func(event, message string)
EventBroker struct {
client page.LifecycleEventClient
handlers map[string][]EventHandler
cancel context.CancelFunc
}
)
func NewEventBroker(client page.LifecycleEventClient) *EventBroker {
return &EventBroker{
client,
make(map[string][]EventHandler),
nil,
}
}
func (broker *EventBroker) Start() error {
if broker.cancel != nil {
return core.Error(core.ErrInvalidOperation, "broker is already started")
}
ctx, cancel := context.WithCancel(context.Background())
broker.cancel = cancel
go func() {
for {
select {
case <-ctx.Done():
return
case <-broker.client.Ready():
reply, err := broker.client.Recv()
if err != nil {
fmt.Println("FAILED TO GET EVENT", err)
broker.Emit("error", err.Error())
return
}
fmt.Println("EVENT", reply.Name)
broker.Emit(reply.Name, "")
}
}
}()
return nil
}
func (broker *EventBroker) Stop() error {
if broker.cancel == nil {
return core.Error(core.ErrInvalidOperation, "broker is already stopped")
}
broker.cancel()
broker.client = nil
return nil
}
func (broker *EventBroker) Close() error {
if broker.cancel != nil {
broker.Stop()
}
return broker.client.Close()
}
func (broker *EventBroker) AddListener(event string, handler EventHandler) {
handlers, ok := broker.handlers[event]
if !ok {
handlers = make([]EventHandler, 0, 5)
broker.handlers[event] = handlers
}
handlers = append(handlers, handler)
}
func (broker *EventBroker) Emit(name, message string) {
handlers, ok := broker.handlers[name]
if !ok {
return
}
for _, handler := range handlers {
handler(name, message)
}
}

View File

@@ -5,8 +5,11 @@ import (
"fmt" "fmt"
"github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/corpix/uarand"
"github.com/mafredri/cdp" "github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom" "github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/emulation"
"github.com/mafredri/cdp/protocol/page"
"github.com/mafredri/cdp/rpcc" "github.com/mafredri/cdp/rpcc"
"strings" "strings"
"time" "time"
@@ -16,6 +19,7 @@ type HtmlDocument struct {
*HtmlElement *HtmlElement
conn *rpcc.Conn conn *rpcc.Conn
client *cdp.Client client *cdp.Client
events *EventBroker
url string url string
} }
@@ -39,6 +43,13 @@ func NewHtmlDocument(
return client.Page.Enable(ctx) return client.Page.Enable(ctx)
}, },
func() error {
return client.Page.SetLifecycleEventsEnabled(
ctx,
page.NewSetLifecycleEventsEnabledArgs(true),
)
},
func() error { func() error {
return client.DOM.Enable(ctx) return client.DOM.Enable(ctx)
}, },
@@ -46,44 +57,93 @@ func NewHtmlDocument(
func() error { func() error {
return client.Runtime.Enable(ctx) return client.Runtime.Enable(ctx)
}, },
func() error {
return client.Emulation.SetUserAgentOverride(
ctx,
emulation.NewSetUserAgentOverrideArgs(uarand.GetRandom()),
)
},
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
loadEventFired, err := client.Page.LoadEventFired(ctx) err = waitForLoadEvent(ctx, client)
if err != nil { if err != nil {
return nil, err return nil, err
} }
root, err := getRootElement(ctx, client)
if err != nil {
return nil, err
}
events, err := createEventBroker(ctx, client)
if err != nil {
return nil, err
}
doc := &HtmlDocument{
NewHtmlElement(client, root.NodeID, root),
conn,
client,
events,
url,
}
doc.init()
return doc, nil
}
func waitForLoadEvent(ctx context.Context, client *cdp.Client) error {
loadEventFired, err := client.Page.LoadEventFired(ctx)
if err != nil {
return err
}
_, err = loadEventFired.Recv() _, err = loadEventFired.Recv()
if err != nil { if err != nil {
return nil, err return err
} }
loadEventFired.Close() return loadEventFired.Close()
}
func getRootElement(ctx context.Context, client *cdp.Client) (dom.Node, error) {
args := dom.NewGetDocumentArgs() args := dom.NewGetDocumentArgs()
args.Depth = PointerInt(-1) // lets load the entire document args.Depth = PointerInt(-1) // lets load the entire document
d, err := client.DOM.GetDocument(ctx, args) d, err := client.DOM.GetDocument(ctx, args)
if err != nil {
return dom.Node{}, err
}
return d.Root, nil
}
func createEventBroker(ctx context.Context, client *cdp.Client) (*EventBroker, error) {
lfc, err := client.Page.LifecycleEvent(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &HtmlDocument{ return NewEventBroker(lfc), nil
&HtmlElement{client, d.Root.NodeID, d.Root, nil},
conn,
client,
url,
}, nil
} }
func (doc *HtmlDocument) Close() error { func (doc *HtmlDocument) Close() error {
doc.events.Stop()
doc.events.Close()
doc.client.Page.Close(context.Background()) doc.client.Page.Close(context.Background())
return doc.conn.Close() return doc.conn.Close()
@@ -112,6 +172,36 @@ func (doc *HtmlDocument) Compare(other core.Value) int {
} }
} }
func (doc *HtmlDocument) ClickBySelector(selector values.String) (values.Boolean, error) {
res, err := Eval(
doc.client,
fmt.Sprintf(`
var el = document.querySelector("%s");
if (el == null) {
return false;
}
var evt = new window.MouseEvent('click', { bubbles: true });
el.dispatchEvent(evt);
return true;
`, selector),
true,
false,
)
if err != nil {
return values.False, err
}
if res.Type() == core.BooleanType {
return res.(values.Boolean), nil
}
return values.False, nil
}
func (doc *HtmlDocument) WaitForSelector(selector values.String, timeout values.Int) error { func (doc *HtmlDocument) WaitForSelector(selector values.String, timeout values.Int) error {
task := NewWaitTask( task := NewWaitTask(
doc.client, doc.client,
@@ -125,9 +215,27 @@ func (doc *HtmlDocument) WaitForSelector(selector values.String, timeout values.
return null; return null;
`, selector), `, selector),
time.Millisecond*time.Duration(timeout), time.Millisecond*time.Duration(timeout),
DefaultPolling,
) )
_, err := task.Run() _, err := task.Run()
return err return err
} }
func (doc *HtmlDocument) init() {
// doc.events.AddListener("")
}
func (doc *HtmlDocument) reload() error {
root, err := getRootElement(context.Background(), doc.client)
if err != nil {
return err
}
doc.url = *root.BaseURL
doc.id = root.NodeID
return nil
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/mafredri/cdp" "github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom" "github.com/mafredri/cdp/protocol/dom"
"log"
"strconv" "strconv"
"time" "time"
) )
@@ -21,11 +20,15 @@ const DefaultTimeout = time.Second * 30
type HtmlElement struct { type HtmlElement struct {
client *cdp.Client client *cdp.Client
id dom.NodeID id dom.NodeID
node dom.Node nodeType values.Int
nodeName values.String
value string
attributes *values.Object attributes *values.Object
children []dom.NodeID
loadedChildren *values.Array
} }
func NewHtmlElement( func LoadElement(
client *cdp.Client, client *cdp.Client,
id dom.NodeID, id dom.NodeID,
) (*HtmlElement, error) { ) (*HtmlElement, error) {
@@ -42,15 +45,46 @@ func NewHtmlElement(
dom. dom.
NewDescribeNodeArgs(). NewDescribeNodeArgs().
SetNodeID(id). SetNodeID(id).
SetDepth(-1), SetDepth(1),
) )
if err != nil { if err != nil {
log.Println("ERROR:", err)
return nil, core.Error(err, strconv.Itoa(int(id))) return nil, core.Error(err, strconv.Itoa(int(id)))
} }
return &HtmlElement{client, id, node.Node, nil}, nil return NewHtmlElement(client, id, node.Node), nil
}
func NewHtmlElement(
client *cdp.Client,
id dom.NodeID,
node dom.Node,
) *HtmlElement {
el := new(HtmlElement)
el.client = client
el.id = id
el.nodeType = values.NewInt(node.NodeType)
el.nodeName = values.NewString(node.NodeName)
el.value = ""
el.attributes = parseAttrs(node.Attributes)
var childCount int
if node.ChildNodeCount != nil {
childCount = *node.ChildNodeCount
}
if node.Value != nil {
el.value = *node.Value
}
el.children = make([]dom.NodeID, childCount)
for idx, child := range node.Children {
el.children[idx] = child.NodeID
}
return el
} }
func (el *HtmlElement) Close() error { func (el *HtmlElement) Close() error {
@@ -69,19 +103,19 @@ func (el *HtmlElement) MarshalJSON() ([]byte, error) {
defer cancelFn() defer cancelFn()
args := dom.NewGetOuterHTMLArgs() args := dom.NewGetOuterHTMLArgs()
args.NodeID = &el.node.NodeID args.NodeID = &el.id
reply, err := el.client.DOM.GetOuterHTML(ctx, args) reply, err := el.client.DOM.GetOuterHTML(ctx, args)
if err != nil { if err != nil {
return nil, core.Error(err, strconv.Itoa(int(el.node.NodeID))) return nil, core.Error(err, strconv.Itoa(int(el.id)))
} }
return json.Marshal(reply.OuterHTML) return json.Marshal(reply.OuterHTML)
} }
func (el *HtmlElement) String() string { func (el *HtmlElement) String() string {
return *el.node.Value return el.value
} }
func (el *HtmlElement) Compare(other core.Value) int { func (el *HtmlElement) Compare(other core.Value) int {
@@ -89,8 +123,8 @@ func (el *HtmlElement) Compare(other core.Value) int {
case core.HtmlDocumentType: case core.HtmlDocumentType:
other := other.(*HtmlElement) other := other.(*HtmlElement)
id := int(el.node.NodeID) id := int(el.id)
otherId := int(other.node.NodeID) otherId := int(other.id)
if id == otherId { if id == otherId {
return 0 return 0
@@ -111,13 +145,13 @@ func (el *HtmlElement) Compare(other core.Value) int {
} }
func (el *HtmlElement) Unwrap() interface{} { func (el *HtmlElement) Unwrap() interface{} {
return el.node return el
} }
func (el *HtmlElement) Hash() int { func (el *HtmlElement) Hash() int {
h := sha512.New() h := sha512.New()
out, err := h.Write([]byte(*el.node.Value)) out, err := h.Write([]byte(el.value))
if err != nil { if err != nil {
return 0 return 0
@@ -131,34 +165,22 @@ func (el *HtmlElement) Value() core.Value {
} }
func (el *HtmlElement) Length() values.Int { func (el *HtmlElement) Length() values.Int {
if el.node.ChildNodeCount == nil { return values.NewInt(len(el.children))
return values.ZeroInt
}
return values.NewInt(*el.node.ChildNodeCount)
} }
func (el *HtmlElement) NodeType() values.Int { func (el *HtmlElement) NodeType() values.Int {
return values.NewInt(el.node.NodeType) return el.nodeType
} }
func (el *HtmlElement) NodeName() values.String { func (el *HtmlElement) NodeName() values.String {
return values.NewString(el.node.NodeName) return el.nodeName
} }
func (el *HtmlElement) GetAttributes() core.Value { func (el *HtmlElement) GetAttributes() core.Value {
if el.attributes == nil {
el.attributes = el.parseAttrs()
}
return el.attributes return el.attributes
} }
func (el *HtmlElement) GetAttribute(name values.String) core.Value { func (el *HtmlElement) GetAttribute(name values.String) core.Value {
if el.attributes == nil {
el.attributes = el.parseAttrs()
}
val, found := el.attributes.Get(name) val, found := el.attributes.Get(name)
if !found { if !found {
@@ -169,27 +191,19 @@ func (el *HtmlElement) GetAttribute(name values.String) core.Value {
} }
func (el *HtmlElement) GetChildNodes() core.Value { func (el *HtmlElement) GetChildNodes() core.Value {
arr := values.NewArray(len(el.node.Children)) if el.loadedChildren == nil {
el.loadedChildren = loadNodes(el.client, el.children)
for idx := range el.node.Children {
el := el.GetChildNode(values.NewInt(idx))
if el != values.None {
arr.Push(el)
}
} }
return arr return el.loadedChildren
} }
func (el *HtmlElement) GetChildNode(idx values.Int) core.Value { func (el *HtmlElement) GetChildNode(idx values.Int) core.Value {
if el.Length() < idx { if el.loadedChildren == nil {
return values.None el.loadedChildren = loadNodes(el.client, el.children)
} }
childNode := el.node.Children[idx] return el.loadedChildren.Get(idx)
return &HtmlElement{el.client, childNode.NodeID, childNode, nil}
} }
func (el *HtmlElement) QuerySelector(selector values.String) core.Value { func (el *HtmlElement) QuerySelector(selector values.String) core.Value {
@@ -199,11 +213,10 @@ func (el *HtmlElement) QuerySelector(selector values.String) core.Value {
found, err := el.client.DOM.QuerySelector(ctx, selectorArgs) found, err := el.client.DOM.QuerySelector(ctx, selectorArgs)
if err != nil { if err != nil {
el.logErr(err, selector.String())
return values.None return values.None
} }
res, err := NewHtmlElement(el.client, found.NodeID) res, err := LoadElement(el.client, found.NodeID)
if err != nil { if err != nil {
return values.None return values.None
@@ -219,14 +232,13 @@ func (el *HtmlElement) QuerySelectorAll(selector values.String) core.Value {
res, err := el.client.DOM.QuerySelectorAll(ctx, selectorArgs) res, err := el.client.DOM.QuerySelectorAll(ctx, selectorArgs)
if err != nil { if err != nil {
el.logErr(err, selector.String())
return values.None return values.None
} }
arr := values.NewArray(len(res.NodeIDs)) arr := values.NewArray(len(res.NodeIDs))
for _, id := range res.NodeIDs { for _, id := range res.NodeIDs {
childEl, err := NewHtmlElement(el.client, id) childEl, err := LoadElement(el.client, id)
if err != nil { if err != nil {
return values.None return values.None
@@ -257,31 +269,37 @@ func (el *HtmlElement) InnerText() values.String {
} }
func (el *HtmlElement) InnerHtml() values.String { func (el *HtmlElement) InnerHtml() values.String {
ctx, cancelFn := el.createCtx() ctx, cancelFn := createCtx()
defer cancelFn() defer cancelFn()
res, err := el.client.DOM.GetOuterHTML(ctx, dom.NewGetOuterHTMLArgs().SetNodeID(el.id)) res, err := el.client.DOM.GetOuterHTML(ctx, dom.NewGetOuterHTMLArgs().SetNodeID(el.id))
if err != nil { if err != nil {
el.logErr(err)
return values.EmptyString return values.EmptyString
} }
return values.NewString(res.OuterHTML) return values.NewString(res.OuterHTML)
} }
func (el *HtmlElement) createCtx() (context.Context, context.CancelFunc) { func (el *HtmlElement) Click() (values.Boolean, error) {
ctx, cancel := createCtx()
defer cancel()
return DispatchEvent(ctx, el.client, el.id, "click")
}
func createCtx() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), DefaultTimeout) return context.WithTimeout(context.Background(), DefaultTimeout)
} }
func (el *HtmlElement) parseAttrs() *values.Object { func parseAttrs(attrs []string) *values.Object {
var attr values.String var attr values.String
res := values.NewObject() res := values.NewObject()
for _, el := range el.node.Attributes { for _, el := range attrs {
str := values.NewString(el) str := values.NewString(el)
if common.IsAttribute(el) { if common.IsAttribute(el) {
@@ -299,11 +317,18 @@ func (el *HtmlElement) parseAttrs() *values.Object {
return res return res
} }
func (el *HtmlElement) logErr(values ...interface{}) { func loadNodes(client *cdp.Client, nodes []dom.NodeID) *values.Array {
args := make([]interface{}, 0, len(values)+1) arr := values.NewArray(len(nodes))
args = append(args, "ERROR:")
args = append(args, values...)
args = append(args, "id:", el.node.NodeID)
log.Println(args...) for _, id := range nodes {
child, err := LoadElement(client, id)
if err != nil {
break
}
arr.Push(child)
}
return arr
} }

View File

@@ -1,6 +1,16 @@
package browser package browser
import "golang.org/x/sync/errgroup" import (
"context"
"encoding/json"
"fmt"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/runtime"
"golang.org/x/sync/errgroup"
)
func PointerInt(input int) *int { func PointerInt(input int) *int {
return &input return &input
@@ -17,3 +27,107 @@ func RunBatch(funcs ...BatchFunc) error {
return eg.Wait() return eg.Wait()
} }
func PrepareEval(exp string) string {
return fmt.Sprintf("((function () {%s})())", exp)
}
func Eval(client *cdp.Client, exp string, ret bool, async bool) (core.Value, error) {
args := runtime.
NewEvaluateArgs(PrepareEval(exp)).
SetReturnByValue(ret).
SetAwaitPromise(async)
out, err := client.Runtime.Evaluate(context.Background(), args)
if err != nil {
return values.None, err
}
if out.ExceptionDetails != nil {
ex := out.ExceptionDetails
return values.None, core.Error(
core.ErrUnexpected,
fmt.Sprintf("%s: %s", ex.Text, *ex.Exception.Description),
)
}
if out.Result.Type != "undefined" {
var o interface{}
err := json.Unmarshal(out.Result.Value, &o)
if err != nil {
return values.None, core.Error(core.ErrUnexpected, err.Error())
}
return values.Parse(o), nil
}
return values.None, nil
}
func DispatchEvent(
ctx context.Context,
client *cdp.Client,
id dom.NodeID,
eventName string,
) (values.Boolean, error) {
// get a ref to remote object representing the node
obj, err := client.DOM.ResolveNode(
ctx,
dom.NewResolveNodeArgs().
SetNodeID(id),
)
if err != nil {
return values.False, err
}
if obj.Object.ObjectID == nil {
return values.False, nil
}
evt, err := client.Runtime.Evaluate(ctx, runtime.NewEvaluateArgs(PrepareEval(fmt.Sprintf(`
return new window.MouseEvent('%s', { bubbles: true })
`, eventName))))
if err != nil {
return values.False, nil
}
if evt.ExceptionDetails != nil {
return values.False, evt.ExceptionDetails
}
if evt.Result.ObjectID == nil {
return values.False, nil
}
evtId := evt.Result.ObjectID
// release the event object
defer client.Runtime.ReleaseObject(ctx, runtime.NewReleaseObjectArgs(*evtId))
res, err := client.Runtime.CallFunctionOn(
ctx,
runtime.NewCallFunctionOnArgs("dispatchEvent").
SetObjectID(*obj.Object.ObjectID).
SetArguments([]runtime.CallArgument{
{
ObjectID: evt.Result.ObjectID,
},
}),
)
if err != nil {
return values.False, err
}
if res.ExceptionDetails != nil {
return values.False, res.ExceptionDetails
}
return values.True, nil
}

View File

@@ -1,13 +1,9 @@
package browser package browser
import ( import (
"context"
"encoding/json"
"fmt"
"github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/mafredri/cdp" "github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/runtime"
"time" "time"
) )
@@ -15,82 +11,65 @@ type WaitTask struct {
client *cdp.Client client *cdp.Client
predicate string predicate string
timeout time.Duration timeout time.Duration
polling time.Duration
} }
const DefaultPolling = time.Millisecond * time.Duration(200)
func NewWaitTask( func NewWaitTask(
client *cdp.Client, client *cdp.Client,
predicate string, predicate string,
timeout time.Duration, timeout time.Duration,
polling time.Duration,
) *WaitTask { ) *WaitTask {
return &WaitTask{ return &WaitTask{
client, client,
fmt.Sprintf("((function () {%s})())", predicate), predicate,
timeout, timeout,
polling,
} }
} }
func (task *WaitTask) Run() (core.Value, error) { func (task *WaitTask) Run() (core.Value, error) {
var result core.Value = values.None
var err error
var done bool
timer := time.NewTimer(task.timeout) timer := time.NewTimer(task.timeout)
for !done { for {
select { select {
case <-timer.C: case <-timer.C:
err = core.ErrTimeout return values.None, core.ErrTimeout
done = true
default: default:
out, e := task.exec() out, err := task.eval()
if e != nil {
done = true
timer.Stop()
err = e
break
}
if out != values.None {
timer.Stop()
result = out
done = true
break
}
}
}
return result, err
}
func (task *WaitTask) exec() (core.Value, error) {
args := runtime.NewEvaluateArgs(task.predicate).SetReturnByValue(true)
out, err := task.client.Runtime.Evaluate(context.Background(), args)
// JS expression failed
// terminating
if err != nil { if err != nil {
timer.Stop()
return values.None, err return values.None, err
} }
if out.ExceptionDetails != nil { // JS output is not empty
ex := out.ExceptionDetails // terminating
return values.None, core.Error( if out != values.None {
core.ErrUnexpected, timer.Stop()
fmt.Sprintf("%s %s", ex.Text, *ex.Exception.Description),
) return out, nil
} }
if out.Result.Type != "undefined" { // Nothing yet, let's wait before the next try
var o interface{} time.Sleep(task.polling)
}
err := json.Unmarshal(out.Result.Value, &o)
if err != nil {
return values.None, core.Error(core.ErrUnexpected, err.Error())
} }
return values.Parse(o), nil // TODO: Do we need this code?
} return values.None, core.ErrTimeout
}
return values.None, nil
func (task *WaitTask) eval() (core.Value, error) {
return Eval(
task.client,
task.predicate,
true,
false,
)
} }

View File

@@ -16,7 +16,7 @@ func WaitElement(_ context.Context, args ...core.Value) (core.Value, error) {
arg := args[0] arg := args[0]
selector := args[1].String() selector := args[1].String()
timeout := values.NewInt(1000) timeout := values.NewInt(5000)
if len(args) > 2 { if len(args) > 2 {
if args[2].Type() == core.IntType { if args[2].Type() == core.IntType {
@@ -30,7 +30,11 @@ func WaitElement(_ context.Context, args ...core.Value) (core.Value, error) {
return values.None, err return values.None, err
} }
doc := arg.(*browser.HtmlDocument) doc, ok := arg.(*browser.HtmlDocument)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return values.None, doc.WaitForSelector(values.NewString(selector), timeout) return values.None, doc.WaitForSelector(values.NewString(selector), timeout)
} }

View File

@@ -9,5 +9,6 @@ func NewLib() map[string]core.Function {
"ELEMENT": Element, "ELEMENT": Element,
"ELEMENTS": Elements, "ELEMENTS": Elements,
"WAIT_ELEMENT": WaitElement, "WAIT_ELEMENT": WaitElement,
"CLICK": Click,
} }
} }

View File

@@ -4,7 +4,7 @@ import "github.com/MontFerret/ferret/pkg/runtime/core"
func NewLib() map[string]core.Function { func NewLib() map[string]core.Function {
return map[string]core.Function{ return map[string]core.Function{
"SLEEP": Sleep, "WAIT": Wait,
"LOG": Log, "LOG": Log,
} }
} }

View File

@@ -8,7 +8,7 @@ import (
"time" "time"
) )
func Sleep(_ context.Context, inputs ...core.Value) (core.Value, error) { func Wait(_ context.Context, inputs ...core.Value) (core.Value, error) {
err := core.ValidateArgs(inputs, 1, 1) err := core.ValidateArgs(inputs, 1, 1)
if err != nil { if err != nil {