1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-07-03 00:46:51 +02:00

Refactored dynamic elements

This commit is contained in:
Tim Voronov
2018-09-26 22:03:06 -04:00
parent 5cad22e3b3
commit 825c33010c
22 changed files with 412 additions and 219 deletions

View File

@ -0,0 +1,413 @@
package dynamic
import (
"context"
"crypto/sha512"
"fmt"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic/eval"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic/events"
"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"
"github.com/pkg/errors"
"strings"
"sync"
"time"
)
type HtmlDocument struct {
sync.Mutex
conn *rpcc.Conn
client *cdp.Client
events *events.EventBroker
url string
element *HtmlElement
history []*HtmlElement
}
func LoadHtmlDocument(
ctx context.Context,
conn *rpcc.Conn,
url string,
) (*HtmlDocument, error) {
if conn == nil {
return nil, core.Error(core.ErrMissedArgument, "connection")
}
if url == "" {
return nil, core.Error(core.ErrMissedArgument, "url")
}
client := cdp.NewClient(conn)
err := runBatch(
func() error {
return client.Page.Enable(ctx)
},
func() error {
return client.Page.SetLifecycleEventsEnabled(
ctx,
page.NewSetLifecycleEventsEnabledArgs(true),
)
},
func() error {
return client.DOM.Enable(ctx)
},
func() error {
return client.Runtime.Enable(ctx)
},
func() error {
return client.Emulation.SetUserAgentOverride(
ctx,
emulation.NewSetUserAgentOverrideArgs(uarand.GetRandom()),
)
},
)
if err != nil {
return nil, err
}
err = waitForLoadEvent(ctx, client)
if err != nil {
return nil, err
}
root, innerHtml, err := getRootElement(client)
if err != nil {
return nil, err
}
broker, err := createEventBroker(client)
if err != nil {
return nil, err
}
return NewHtmlDocument(conn, client, broker, root, innerHtml), nil
}
func getRootElement(client *cdp.Client) (dom.Node, values.String, error) {
args := dom.NewGetDocumentArgs()
args.Depth = pointerInt(1) // lets load the entire document
ctx := context.Background()
d, err := client.DOM.GetDocument(ctx, args)
if err != nil {
return dom.Node{}, values.EmptyString, err
}
innerHtml, err := client.DOM.GetOuterHTML(ctx, dom.NewGetOuterHTMLArgs().SetNodeID(d.Root.NodeID))
if err != nil {
return dom.Node{}, values.EmptyString, err
}
return d.Root, values.NewString(innerHtml.OuterHTML), nil
}
func NewHtmlDocument(
conn *rpcc.Conn,
client *cdp.Client,
broker *events.EventBroker,
root dom.Node,
innerHtml values.String,
) *HtmlDocument {
doc := new(HtmlDocument)
doc.conn = conn
doc.client = client
doc.events = broker
doc.element = NewHtmlElement(client, broker, root.NodeID, root, innerHtml)
doc.history = make([]*HtmlElement, 0, 10)
doc.url = ""
if root.BaseURL != nil {
doc.url = *root.BaseURL
}
broker.AddEventListener("load", func(_ interface{}) {
doc.Lock()
defer doc.Unlock()
updated, innerHtml, err := getRootElement(client)
if err != nil {
// TODO: We need somehow log all errors outside of stdout
return
}
// put the root element in a history list, since it might be still used
doc.history = append(doc.history, doc.element)
// create a new root element wrapper
doc.element = NewHtmlElement(client, broker, updated.NodeID, updated, innerHtml)
doc.url = ""
if updated.BaseURL != nil {
doc.url = *updated.BaseURL
}
})
return doc
}
func (doc *HtmlDocument) MarshalJSON() ([]byte, error) {
doc.Lock()
defer doc.Unlock()
return doc.element.MarshalJSON()
}
func (doc *HtmlDocument) Type() core.Type {
return core.HtmlDocumentType
}
func (doc *HtmlDocument) String() string {
doc.Lock()
defer doc.Unlock()
return doc.url
}
func (doc *HtmlDocument) Unwrap() interface{} {
doc.Lock()
defer doc.Unlock()
return doc.element
}
func (doc *HtmlDocument) Hash() int {
doc.Lock()
defer doc.Unlock()
h := sha512.New()
out, err := h.Write([]byte(doc.url))
if err != nil {
return 0
}
return out
}
func (doc *HtmlDocument) Compare(other core.Value) int {
doc.Lock()
defer doc.Unlock()
switch other.Type() {
case core.HtmlDocumentType:
other := other.(*HtmlDocument)
return strings.Compare(doc.url, other.url)
default:
if other.Type() > core.HtmlDocumentType {
return -1
}
return 1
}
}
func (doc *HtmlDocument) Close() error {
doc.Lock()
defer doc.Unlock()
doc.events.Stop()
doc.events.Close()
for _, h := range doc.history {
h.Close()
}
doc.element.Close()
doc.client.Page.Close(context.Background())
return doc.conn.Close()
}
func (doc *HtmlDocument) NodeType() values.Int {
doc.Lock()
defer doc.Unlock()
return doc.element.NodeType()
}
func (doc *HtmlDocument) NodeName() values.String {
doc.Lock()
defer doc.Unlock()
return doc.element.NodeName()
}
func (doc *HtmlDocument) Length() values.Int {
doc.Lock()
defer doc.Unlock()
return doc.element.Length()
}
func (doc *HtmlDocument) InnerText() values.String {
doc.Lock()
defer doc.Unlock()
return doc.element.InnerText()
}
func (doc *HtmlDocument) InnerHtml() values.String {
doc.Lock()
defer doc.Unlock()
return doc.element.InnerHtml()
}
func (doc *HtmlDocument) Value() core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.Value()
}
func (doc *HtmlDocument) GetAttributes() core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.GetAttributes()
}
func (doc *HtmlDocument) GetAttribute(name values.String) core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.GetAttribute(name)
}
func (doc *HtmlDocument) GetChildNodes() core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.GetChildNodes()
}
func (doc *HtmlDocument) GetChildNode(idx values.Int) core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.GetChildNode(idx)
}
func (doc *HtmlDocument) QuerySelector(selector values.String) core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.QuerySelector(selector)
}
func (doc *HtmlDocument) QuerySelectorAll(selector values.String) core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.QuerySelectorAll(selector)
}
func (doc *HtmlDocument) ClickBySelector(selector values.String) (values.Boolean, error) {
res, err := eval.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 := events.NewWaitTask(
doc.client,
fmt.Sprintf(`
el = document.querySelector("%s");
if (el != null) {
return true;
}
return null;
`, selector),
time.Millisecond*time.Duration(timeout),
events.DefaultPolling,
)
_, err := task.Run()
return err
}
func (doc *HtmlDocument) WaitForNavigation(timeout values.Int) error {
timer := time.NewTimer(time.Millisecond * time.Duration(timeout))
onEvent := make(chan bool)
listener := func(_ interface{}) {
onEvent <- true
}
defer doc.events.RemoveEventListener("load", listener)
defer close(onEvent)
doc.events.AddEventListener("load", listener)
for {
select {
case <-onEvent:
timer.Stop()
return nil
case <-timer.C:
return core.ErrTimeout
}
}
}
func (doc *HtmlDocument) Navigate(url values.String) error {
ctx := context.Background()
repl, err := doc.client.Page.Navigate(ctx, page.NewNavigateArgs(url.String()))
if err != nil {
return err
}
if repl.ErrorText != nil {
return errors.New(*repl.ErrorText)
}
return waitForLoadEvent(ctx, doc.client)
}

View File

@ -0,0 +1,116 @@
package dynamic
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/devtool"
"github.com/mafredri/cdp/protocol/target"
"github.com/mafredri/cdp/rpcc"
"github.com/mafredri/cdp/session"
"github.com/pkg/errors"
"sync"
)
type Driver struct {
sync.Mutex
dev *devtool.DevTools
conn *rpcc.Conn
client *cdp.Client
session *session.Manager
contextID target.BrowserContextID
}
func NewDriver(address string) *Driver {
drv := new(Driver)
drv.dev = devtool.New(address)
return drv
}
func (drv *Driver) GetDocument(ctx context.Context, url string) (values.HtmlNode, error) {
err := drv.init(ctx)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
defer cancel()
// Create a new target belonging to the browser context, similar
// to opening a new tab in an incognito window.
createTargetArgs := target.NewCreateTargetArgs(url).SetBrowserContextID(drv.contextID)
createTarget, err := drv.client.Target.CreateTarget(ctx, createTargetArgs)
if err != nil {
return nil, err
}
// Connect to target using the existing websocket connection.
conn, err := drv.session.Dial(ctx, createTarget.TargetID)
if err != nil {
return nil, err
}
return LoadHtmlDocument(ctx, conn, url)
}
func (drv *Driver) Close() error {
drv.Lock()
defer drv.Unlock()
if drv.session != nil {
drv.session.Close()
return drv.conn.Close()
}
return nil
}
func (drv *Driver) init(ctx context.Context) error {
drv.Lock()
defer drv.Unlock()
if drv.session == nil {
ver, err := drv.dev.Version(ctx)
if err != nil {
return errors.Wrap(err, "failed to initialize driver")
}
bconn, err := rpcc.DialContext(ctx, ver.WebSocketDebuggerURL)
if err != nil {
return errors.Wrap(err, "failed to initialize driver")
}
bc := cdp.NewClient(bconn)
sess, err := session.NewManager(bc)
if err != nil {
bconn.Close()
return errors.Wrap(err, "failed to initialize driver")
}
createCtx, err := bc.Target.CreateBrowserContext(ctx)
if err != nil {
bconn.Close()
sess.Close()
return err
}
drv.conn = bconn
drv.client = bc
drv.session = sess
drv.contextID = createCtx.BrowserContextID
}
return nil
}

View File

@ -0,0 +1,337 @@
package dynamic
import (
"bytes"
"context"
"crypto/sha512"
"encoding/json"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/common"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic/events"
"github.com/PuerkitoBio/goquery"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"strconv"
"sync"
"time"
)
const DefaultTimeout = time.Second * 30
type HtmlElement struct {
sync.Mutex
client *cdp.Client
broker *events.EventBroker
connected bool
id dom.NodeID
nodeType values.Int
nodeName values.String
innerHtml values.String
innerText *common.LazyValue
value string
attributes *common.LazyValue
children []dom.NodeID
loadedChildren *common.LazyValue
}
func LoadElement(
client *cdp.Client,
broker *events.EventBroker,
id dom.NodeID,
) (*HtmlElement, error) {
if client == nil {
return nil, core.Error(core.ErrMissedArgument, "client")
}
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn()
node, err := client.DOM.DescribeNode(
ctx,
dom.
NewDescribeNodeArgs().
SetNodeID(id).
SetDepth(-1),
)
if err != nil {
return nil, core.Error(err, strconv.Itoa(int(id)))
}
innerHtml, err := client.DOM.GetOuterHTML(
ctx,
dom.NewGetOuterHTMLArgs().SetNodeID(id),
)
if err != nil {
return nil, core.Error(err, strconv.Itoa(int(id)))
}
return NewHtmlElement(
client,
broker,
id,
node.Node,
values.NewString(innerHtml.OuterHTML),
), nil
}
func NewHtmlElement(
client *cdp.Client,
broker *events.EventBroker,
id dom.NodeID,
node dom.Node,
innerHtml values.String,
) *HtmlElement {
el := new(HtmlElement)
el.client = client
el.broker = broker
el.connected = true
el.id = id
el.nodeType = values.NewInt(node.NodeType)
el.nodeName = values.NewString(node.NodeName)
el.innerHtml = innerHtml
el.innerText = common.NewLazyValue(func() (core.Value, error) {
h := el.InnerHtml()
if h == values.EmptyString {
return h, nil
}
buff := bytes.NewBuffer([]byte(h))
parsed, err := goquery.NewDocumentFromReader(buff)
if err != nil {
return values.EmptyString, err
}
return values.NewString(parsed.Text()), nil
})
el.attributes = common.NewLazyValue(func() (core.Value, error) {
return parseAttrs(node.Attributes), nil
})
el.value = ""
el.loadedChildren = common.NewLazyValue(func() (core.Value, error) {
return loadNodes(client, broker, el.children)
})
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 {
return nil
}
func (el *HtmlElement) Type() core.Type {
return core.HtmlElementType
}
func (el *HtmlElement) MarshalJSON() ([]byte, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn()
args := dom.NewGetOuterHTMLArgs()
args.NodeID = &el.id
reply, err := el.client.DOM.GetOuterHTML(ctx, args)
if err != nil {
return nil, core.Error(err, strconv.Itoa(int(el.id)))
}
return json.Marshal(reply.OuterHTML)
}
func (el *HtmlElement) String() string {
return el.value
}
func (el *HtmlElement) Compare(other core.Value) int {
switch other.Type() {
case core.HtmlDocumentType:
other := other.(*HtmlElement)
id := int(el.id)
otherId := int(other.id)
if id == otherId {
return 0
}
if id > otherId {
return 1
}
return -1
default:
if other.Type() > core.HtmlElementType {
return -1
}
return 1
}
}
func (el *HtmlElement) Unwrap() interface{} {
return el
}
func (el *HtmlElement) Hash() int {
h := sha512.New()
out, err := h.Write([]byte(el.value))
if err != nil {
return 0
}
return out
}
func (el *HtmlElement) Value() core.Value {
return values.None
}
func (el *HtmlElement) Length() values.Int {
return values.NewInt(len(el.children))
}
func (el *HtmlElement) NodeType() values.Int {
return el.nodeType
}
func (el *HtmlElement) NodeName() values.String {
return el.nodeName
}
func (el *HtmlElement) GetAttributes() core.Value {
val, err := el.attributes.Value()
if err != nil {
return values.None
}
return val
}
func (el *HtmlElement) GetAttribute(name values.String) core.Value {
attrs, err := el.attributes.Value()
if err != nil {
return values.None
}
val, found := attrs.(*values.Object).Get(name)
if !found {
return values.None
}
return val
}
func (el *HtmlElement) GetChildNodes() core.Value {
val, err := el.loadedChildren.Value()
if err != nil {
return values.NewArray(0)
}
return val
}
func (el *HtmlElement) GetChildNode(idx values.Int) core.Value {
val, err := el.loadedChildren.Value()
if err != nil {
return values.None
}
return val.(*values.Array).Get(idx)
}
func (el *HtmlElement) QuerySelector(selector values.String) core.Value {
ctx := context.Background()
selectorArgs := dom.NewQuerySelectorArgs(el.id, selector.String())
found, err := el.client.DOM.QuerySelector(ctx, selectorArgs)
if err != nil {
return values.None
}
res, err := LoadElement(el.client, el.broker, found.NodeID)
if err != nil {
return values.None
}
return res
}
func (el *HtmlElement) QuerySelectorAll(selector values.String) core.Value {
ctx := context.Background()
selectorArgs := dom.NewQuerySelectorAllArgs(el.id, selector.String())
res, err := el.client.DOM.QuerySelectorAll(ctx, selectorArgs)
if err != nil {
return values.None
}
arr := values.NewArray(len(res.NodeIDs))
for _, id := range res.NodeIDs {
childEl, err := LoadElement(el.client, el.broker, id)
if err != nil {
return values.None
}
arr.Push(childEl)
}
return arr
}
func (el *HtmlElement) InnerText() values.String {
val, err := el.innerText.Value()
if err != nil {
return values.EmptyString
}
return val.(values.String)
}
func (el *HtmlElement) InnerHtml() values.String {
return el.innerHtml
}
func (el *HtmlElement) Click() (values.Boolean, error) {
ctx, cancel := contextWithTimeout()
defer cancel()
return events.DispatchEvent(ctx, el.client, el.id, "click")
}

View File

@ -0,0 +1,118 @@
package eval
import (
"context"
"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"
)
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" {
return values.Unmarshal(out.Result.Value)
}
return Unmarshal(&out.Result)
}
func Property(
ctx context.Context,
client *cdp.Client,
id dom.NodeID,
propName string,
) (core.Value, 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.None, err
}
if obj.Object.ObjectID == nil {
return values.None, core.Error(core.ErrNotFound, fmt.Sprintf("element %d", id))
}
defer client.Runtime.ReleaseObject(ctx, runtime.NewReleaseObjectArgs(*obj.Object.ObjectID))
res, err := client.Runtime.GetProperties(
ctx,
runtime.NewGetPropertiesArgs(*obj.Object.ObjectID),
)
if err != nil {
return values.None, err
}
if res.ExceptionDetails != nil {
return values.None, res.ExceptionDetails
}
// all props
if propName == "" {
var arr *values.Array
arr = values.NewArray(len(res.Result))
for _, prop := range res.Result {
val, err := Unmarshal(prop.Value)
if err != nil {
return values.None, err
}
arr.Push(val)
}
return arr, nil
}
for _, prop := range res.Result {
if prop.Name == propName {
return Unmarshal(prop.Value)
}
}
return values.None, nil
}
func Unmarshal(obj *runtime.RemoteObject) (core.Value, error) {
if obj == nil {
return values.None, nil
}
if obj.Type != "undefined" {
return values.Unmarshal(obj.Value)
}
return values.None, nil
}

View File

@ -0,0 +1,190 @@
package events
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/mafredri/cdp/rpcc"
"reflect"
"sync"
"time"
)
type (
MessageFactory func() interface{}
EventStream struct {
stream rpcc.Stream
message MessageFactory
}
EventListener func(message interface{})
EventBroker struct {
sync.Mutex
events map[string]*EventStream
listeners map[string][]EventListener
cancel context.CancelFunc
}
)
func NewEventBroker() *EventBroker {
broker := new(EventBroker)
broker.events = make(map[string]*EventStream)
broker.listeners = make(map[string][]EventListener)
return broker
}
func (broker *EventBroker) AddEventStream(name string, stream rpcc.Stream, msg MessageFactory) error {
broker.Lock()
defer broker.Unlock()
_, exists := broker.events[name]
if exists {
return core.Error(core.ErrNotUnique, name)
}
broker.events[name] = &EventStream{stream, msg}
return nil
}
func (broker *EventBroker) AddEventListener(event string, listener EventListener) {
broker.Lock()
defer broker.Unlock()
listeners, ok := broker.listeners[event]
if !ok {
listeners = make([]EventListener, 0, 5)
}
broker.listeners[event] = append(listeners, listener)
}
func (broker *EventBroker) RemoveEventListener(event string, listener EventListener) {
broker.Lock()
defer broker.Unlock()
idx := -1
listeners, ok := broker.listeners[event]
if !ok {
return
}
listenerPointer := reflect.ValueOf(listener).Pointer()
for i, l := range listeners {
itemPointer := reflect.ValueOf(l).Pointer()
if itemPointer == listenerPointer {
idx = i
break
}
}
if idx < 0 {
return
}
var modifiedListeners []EventListener = nil
if len(listeners) > 1 {
modifiedListeners = append(listeners[:idx], listeners[idx+1:]...)
}
broker.listeners[event] = modifiedListeners
}
func (broker *EventBroker) Start() error {
broker.Lock()
defer broker.Unlock()
if broker.cancel != nil {
return core.Error(core.ErrInvalidOperation, "broker is already started")
}
ctx, cancel := context.WithCancel(context.Background())
broker.cancel = cancel
go func() {
counter := 0
eventsCount := len(broker.events)
for {
for name, event := range broker.events {
counter += 1
select {
case <-ctx.Done():
return
case <-event.stream.Ready():
msg := event.message()
err := event.stream.RecvMsg(msg)
if err != nil {
broker.emit("error", err)
return
}
broker.emit(name, msg)
default:
// we have iterated over all events
// lets pause
if counter == eventsCount {
counter = 0
time.Sleep(DefaultPolling)
}
continue
}
}
}
}()
return nil
}
func (broker *EventBroker) Stop() error {
broker.Lock()
defer broker.Unlock()
if broker.cancel == nil {
return core.Error(core.ErrInvalidOperation, "broker is already stopped")
}
broker.cancel()
return nil
}
func (broker *EventBroker) Close() error {
broker.Lock()
defer broker.Unlock()
if broker.cancel != nil {
broker.cancel()
}
for _, event := range broker.events {
event.stream.Close()
}
return nil
}
func (broker *EventBroker) emit(name string, message interface{}) {
broker.Lock()
defer broker.Unlock()
listeners, ok := broker.listeners[name]
if !ok {
return
}
for _, listener := range listeners {
listener(message)
}
}

View File

@ -0,0 +1,75 @@
package events
import (
"context"
"fmt"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic/eval"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/runtime"
)
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(eval.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

@ -0,0 +1,69 @@
package events
import (
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic/eval"
"github.com/mafredri/cdp"
"time"
)
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,
predicate,
timeout,
polling,
}
}
func (task *WaitTask) Run() (core.Value, error) {
timer := time.NewTimer(task.timeout)
for {
select {
case <-timer.C:
return values.None, core.ErrTimeout
default:
out, err := eval.Eval(
task.client,
task.predicate,
true,
false,
)
// JS expression failed
// terminating
if err != nil {
timer.Stop()
return values.None, err
}
// JS output is not empty
// terminating
if out != values.None {
timer.Stop()
return out, nil
}
// Nothing yet, let's wait before the next try
time.Sleep(task.polling)
}
}
}

View File

@ -0,0 +1,182 @@
package dynamic
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/common"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic/events"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/page"
"golang.org/x/sync/errgroup"
)
func pointerInt(input int) *int {
return &input
}
type batchFunc = func() error
func runBatch(funcs ...batchFunc) error {
eg := errgroup.Group{}
for _, f := range funcs {
eg.Go(f)
}
return eg.Wait()
}
func parseAttrs(attrs []string) *values.Object {
var attr values.String
res := values.NewObject()
for _, el := range attrs {
str := values.NewString(el)
if common.IsAttribute(el) {
attr = str
res.Set(str, values.EmptyString)
} else {
current, ok := res.Get(attr)
if ok {
res.Set(attr, current.(values.String).Concat(values.SpaceString).Concat(str))
}
}
}
return res
}
func loadNodes(client *cdp.Client, broker *events.EventBroker, nodes []dom.NodeID) (*values.Array, error) {
arr := values.NewArray(len(nodes))
for _, id := range nodes {
child, err := LoadElement(client, broker, id)
if err != nil {
return nil, err
}
arr.Push(child)
}
return arr, nil
}
func contextWithTimeout() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), DefaultTimeout)
}
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 err
}
return loadEventFired.Close()
}
func createEventBroker(client *cdp.Client) (*events.EventBroker, error) {
ctx := context.Background()
load, err := client.Page.LoadEventFired(ctx)
if err != nil {
return nil, err
}
broker := events.NewEventBroker()
broker.AddEventStream("load", load, func() interface{} {
return new(page.LoadEventFiredReply)
})
err = broker.Start()
if err != nil {
broker.Close()
return nil, err
}
destroy, err := client.DOM.DocumentUpdated(ctx)
if err != nil {
broker.Close()
return nil, err
}
broker.AddEventStream("reload", destroy, func() interface{} {
return new(dom.DocumentUpdatedReply)
})
attrModified, err := client.DOM.AttributeModified(ctx)
if err != nil {
broker.Close()
return nil, err
}
broker.AddEventStream("attr:modified", attrModified, func() interface{} {
return new(dom.AttributeModifiedReply)
})
attrRemoved, err := client.DOM.AttributeRemoved(ctx)
if err != nil {
broker.Close()
return nil, err
}
broker.AddEventStream("attr:removed", attrRemoved, func() interface{} {
return new(dom.AttributeRemovedReply)
})
childrenCount, err := client.DOM.ChildNodeCountUpdated(ctx)
if err != nil {
broker.Close()
return nil, err
}
broker.AddEventStream("children:count", childrenCount, func() interface{} {
return new(dom.ChildNodeCountUpdatedReply)
})
childrenInsert, err := client.DOM.ChildNodeInserted(ctx)
if err != nil {
broker.Close()
return nil, err
}
broker.AddEventStream("children:inserted", childrenInsert, func() interface{} {
return new(dom.ChildNodeInsertedReply)
})
childDeleted, err := client.DOM.ChildNodeRemoved(ctx)
if err != nil {
broker.Close()
return nil, err
}
broker.AddEventStream("children:deleted", childDeleted, func() interface{} {
return new(dom.ChildNodeRemovedReply)
})
return broker, nil
}