1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-07-15 01:25:00 +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

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"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/corpix/uarand"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/emulation"
"github.com/mafredri/cdp/protocol/page"
"github.com/mafredri/cdp/rpcc"
"strings"
"time"
@ -16,6 +19,7 @@ type HtmlDocument struct {
*HtmlElement
conn *rpcc.Conn
client *cdp.Client
events *EventBroker
url string
}
@ -39,6 +43,13 @@ func NewHtmlDocument(
return client.Page.Enable(ctx)
},
func() error {
return client.Page.SetLifecycleEventsEnabled(
ctx,
page.NewSetLifecycleEventsEnabledArgs(true),
)
},
func() error {
return client.DOM.Enable(ctx)
},
@ -46,44 +57,93 @@ func NewHtmlDocument(
func() error {
return client.Runtime.Enable(ctx)
},
func() error {
return client.Emulation.SetUserAgentOverride(
ctx,
emulation.NewSetUserAgentOverrideArgs(uarand.GetRandom()),
)
},
)
if err != nil {
return nil, err
}
loadEventFired, err := client.Page.LoadEventFired(ctx)
err = waitForLoadEvent(ctx, client)
if err != nil {
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()
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.Depth = PointerInt(-1) // lets load the entire document
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 {
return nil, err
}
return &HtmlDocument{
&HtmlElement{client, d.Root.NodeID, d.Root, nil},
conn,
client,
url,
}, nil
return NewEventBroker(lfc), nil
}
func (doc *HtmlDocument) Close() error {
doc.events.Stop()
doc.events.Close()
doc.client.Page.Close(context.Background())
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 {
task := NewWaitTask(
doc.client,
@ -125,9 +215,27 @@ func (doc *HtmlDocument) WaitForSelector(selector values.String, timeout values.
return null;
`, selector),
time.Millisecond*time.Duration(timeout),
DefaultPolling,
)
_, err := task.Run()
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/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"log"
"strconv"
"time"
)
@ -19,13 +18,17 @@ import (
const DefaultTimeout = time.Second * 30
type HtmlElement struct {
client *cdp.Client
id dom.NodeID
node dom.Node
attributes *values.Object
client *cdp.Client
id dom.NodeID
nodeType values.Int
nodeName values.String
value string
attributes *values.Object
children []dom.NodeID
loadedChildren *values.Array
}
func NewHtmlElement(
func LoadElement(
client *cdp.Client,
id dom.NodeID,
) (*HtmlElement, error) {
@ -42,15 +45,46 @@ func NewHtmlElement(
dom.
NewDescribeNodeArgs().
SetNodeID(id).
SetDepth(-1),
SetDepth(1),
)
if err != nil {
log.Println("ERROR:", err)
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 {
@ -69,19 +103,19 @@ func (el *HtmlElement) MarshalJSON() ([]byte, error) {
defer cancelFn()
args := dom.NewGetOuterHTMLArgs()
args.NodeID = &el.node.NodeID
args.NodeID = &el.id
reply, err := el.client.DOM.GetOuterHTML(ctx, args)
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)
}
func (el *HtmlElement) String() string {
return *el.node.Value
return el.value
}
func (el *HtmlElement) Compare(other core.Value) int {
@ -89,8 +123,8 @@ func (el *HtmlElement) Compare(other core.Value) int {
case core.HtmlDocumentType:
other := other.(*HtmlElement)
id := int(el.node.NodeID)
otherId := int(other.node.NodeID)
id := int(el.id)
otherId := int(other.id)
if id == otherId {
return 0
@ -111,13 +145,13 @@ func (el *HtmlElement) Compare(other core.Value) int {
}
func (el *HtmlElement) Unwrap() interface{} {
return el.node
return el
}
func (el *HtmlElement) Hash() int {
h := sha512.New()
out, err := h.Write([]byte(*el.node.Value))
out, err := h.Write([]byte(el.value))
if err != nil {
return 0
@ -131,34 +165,22 @@ func (el *HtmlElement) Value() core.Value {
}
func (el *HtmlElement) Length() values.Int {
if el.node.ChildNodeCount == nil {
return values.ZeroInt
}
return values.NewInt(*el.node.ChildNodeCount)
return values.NewInt(len(el.children))
}
func (el *HtmlElement) NodeType() values.Int {
return values.NewInt(el.node.NodeType)
return el.nodeType
}
func (el *HtmlElement) NodeName() values.String {
return values.NewString(el.node.NodeName)
return el.nodeName
}
func (el *HtmlElement) GetAttributes() core.Value {
if el.attributes == nil {
el.attributes = el.parseAttrs()
}
return el.attributes
}
func (el *HtmlElement) GetAttribute(name values.String) core.Value {
if el.attributes == nil {
el.attributes = el.parseAttrs()
}
val, found := el.attributes.Get(name)
if !found {
@ -169,27 +191,19 @@ func (el *HtmlElement) GetAttribute(name values.String) core.Value {
}
func (el *HtmlElement) GetChildNodes() core.Value {
arr := values.NewArray(len(el.node.Children))
for idx := range el.node.Children {
el := el.GetChildNode(values.NewInt(idx))
if el != values.None {
arr.Push(el)
}
if el.loadedChildren == nil {
el.loadedChildren = loadNodes(el.client, el.children)
}
return arr
return el.loadedChildren
}
func (el *HtmlElement) GetChildNode(idx values.Int) core.Value {
if el.Length() < idx {
return values.None
if el.loadedChildren == nil {
el.loadedChildren = loadNodes(el.client, el.children)
}
childNode := el.node.Children[idx]
return &HtmlElement{el.client, childNode.NodeID, childNode, nil}
return el.loadedChildren.Get(idx)
}
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)
if err != nil {
el.logErr(err, selector.String())
return values.None
}
res, err := NewHtmlElement(el.client, found.NodeID)
res, err := LoadElement(el.client, found.NodeID)
if err != nil {
return values.None
@ -219,14 +232,13 @@ func (el *HtmlElement) QuerySelectorAll(selector values.String) core.Value {
res, err := el.client.DOM.QuerySelectorAll(ctx, selectorArgs)
if err != nil {
el.logErr(err, selector.String())
return values.None
}
arr := values.NewArray(len(res.NodeIDs))
for _, id := range res.NodeIDs {
childEl, err := NewHtmlElement(el.client, id)
childEl, err := LoadElement(el.client, id)
if err != nil {
return values.None
@ -257,31 +269,37 @@ func (el *HtmlElement) InnerText() values.String {
}
func (el *HtmlElement) InnerHtml() values.String {
ctx, cancelFn := el.createCtx()
ctx, cancelFn := createCtx()
defer cancelFn()
res, err := el.client.DOM.GetOuterHTML(ctx, dom.NewGetOuterHTMLArgs().SetNodeID(el.id))
if err != nil {
el.logErr(err)
return values.EmptyString
}
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)
}
func (el *HtmlElement) parseAttrs() *values.Object {
func parseAttrs(attrs []string) *values.Object {
var attr values.String
res := values.NewObject()
for _, el := range el.node.Attributes {
for _, el := range attrs {
str := values.NewString(el)
if common.IsAttribute(el) {
@ -299,11 +317,18 @@ func (el *HtmlElement) parseAttrs() *values.Object {
return res
}
func (el *HtmlElement) logErr(values ...interface{}) {
args := make([]interface{}, 0, len(values)+1)
args = append(args, "ERROR:")
args = append(args, values...)
args = append(args, "id:", el.node.NodeID)
func loadNodes(client *cdp.Client, nodes []dom.NodeID) *values.Array {
arr := values.NewArray(len(nodes))
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
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 {
return &input
@ -17,3 +27,107 @@ func RunBatch(funcs ...BatchFunc) error {
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
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/runtime"
"time"
)
@ -15,82 +11,65 @@ type WaitTask struct {
client *cdp.Client
predicate string
timeout time.Duration
polling time.Duration
}
const DefaultPolling = time.Millisecond * time.Duration(200)
func NewWaitTask(
client *cdp.Client,
predicate string,
timeout time.Duration,
polling time.Duration,
) *WaitTask {
return &WaitTask{
client,
fmt.Sprintf("((function () {%s})())", predicate),
predicate,
timeout,
polling,
}
}
func (task *WaitTask) Run() (core.Value, error) {
var result core.Value = values.None
var err error
var done bool
timer := time.NewTimer(task.timeout)
for !done {
for {
select {
case <-timer.C:
err = core.ErrTimeout
done = true
return values.None, core.ErrTimeout
default:
out, e := task.exec()
out, err := task.eval()
if e != nil {
done = true
// JS expression failed
// terminating
if err != nil {
timer.Stop()
err = e
break
return values.None, err
}
// JS output is not empty
// terminating
if out != values.None {
timer.Stop()
result = out
done = true
break
return out, nil
}
// Nothing yet, let's wait before the next try
time.Sleep(task.polling)
}
}
return result, err
// TODO: Do we need this code?
return values.None, core.ErrTimeout
}
func (task *WaitTask) exec() (core.Value, error) {
args := runtime.NewEvaluateArgs(task.predicate).SetReturnByValue(true)
out, err := task.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 (task *WaitTask) eval() (core.Value, error) {
return Eval(
task.client,
task.predicate,
true,
false,
)
}