Skip to content

Commit

Permalink
replace Evaluate with CallFunctionOn to execuate js on node
Browse files Browse the repository at this point in the history
Note: there is a performance cost here.

Before the changes, just one CDP command is sent:
- Runtime.evaluate

After the changes, three CDP command are sent:
- DOM.resolveNode: to get the RemoteObjectId from NodeId
- Runtime.callFunctionOn
- Runtime.releaseObject
  • Loading branch information
ZekeLu committed Jul 12, 2021
1 parent 936b5f6 commit eb823ff
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 103 deletions.
122 changes: 39 additions & 83 deletions js.go
@@ -1,89 +1,76 @@
package chromedp

import (
"fmt"

"github.com/chromedp/cdproto/cdp"
)

const (
// textJS is a javascript snippet that returns the concatenated innerText of all
// visible (ie, offsetWidth || offsetHeight || getClientRects().length ) children.
textJS = `(function(a) {
var s = '';
for (var i = 0; i < a.length; i++) {
if (a[i].offsetWidth || a[i].offsetHeight || a[i].getClientRects().length) {
s += a[i].innerText;
}
// textJS is a javascript snippet that returns the innerText of the specified
// visible (ie, offsetWidth || offsetHeight || getClientRects().length ) element.
textJS = `function text() {
if (this.offsetWidth || this.offsetHeight || this.getClientRects().length) {
return this.innerText;
}
return s;
})(%s)`
return '';
}`

// textContentJS is a javascript snippet that returns the concatenated textContent
// of all children.
textContentJS = `(function(a) {
var s = '';
for (var i = 0; i < a.length; i++) {
s += a[i].textContent;
}
return s;
})(%s)`
// textContentJS is a javascript snippet that returns the textContent of the
// specified element.
textContentJS = `function textContent() {
return this.textContent;
}`

// blurJS is a javascript snippet that blurs the specified element.
blurJS = `(function(a) {
a.blur();
blurJS = `function blur() {
this.blur();
return true;
})(%s)`
}`

// submitJS is a javascript snippet that will call the containing form's
// submit function, returning true or false if the call was successful.
submitJS = `(function(a) {
if (a.nodeName === 'FORM') {
HTMLFormElement.prototype.submit.call(a);
submitJS = `function submit() {
if (this.nodeName === 'FORM') {
HTMLFormElement.prototype.submit.call(this);
return true;
} else if (a.form !== null) {
HTMLFormElement.prototype.submit.call(a.form);
} else if (this.form !== null) {
HTMLFormElement.prototype.submit.call(this.form);
return true;
}
return false;
})(%s)`
}`

// resetJS is a javascript snippet that will call the containing form's
// reset function, returning true or false if the call was successful.
resetJS = `(function(a) {
if (a.nodeName === 'FORM') {
HTMLFormElement.prototype.reset.call(a);
resetJS = `function reset() {
if (this.nodeName === 'FORM') {
HTMLFormElement.prototype.reset.call(this);
return true;
} else if (a.form !== null) {
HTMLFormElement.prototype.reset.call(a.form);
} else if (this.form !== null) {
HTMLFormElement.prototype.reset.call(this.form);
return true;
}
return false;
})(%s)`
}`

// attributeJS is a javascript snippet that returns the attribute of a specified
// node.
attributeJS = `(function(a, n) {
return a[n];
})(%s, %q)`
attributeJS = `function attribute(n) {
return this[n];
}`

// setAttributeJS is a javascript snippet that sets the value of the specified
// node, and returns the value.
setAttributeJS = `(function(a, n, v) {
a[n] = v;
setAttributeJS = `function setAttribute(n, v) {
this[n] = v;
if (n === 'value') {
a.dispatchEvent(new Event('input', { bubbles: true }));
a.dispatchEvent(new Event('change', { bubbles: true }));
this.dispatchEvent(new Event('input', { bubbles: true }));
this.dispatchEvent(new Event('change', { bubbles: true }));
}
return a[n];
})(%s, %q, %q)`
return this[n];
}`

// visibleJS is a javascript snippet that returns true or false depending on if
// the specified node's offsetWidth, offsetHeight or getClientRects().length is
// not null.
visibleJS = `(function(a) {
return Boolean( a.offsetWidth || a.offsetHeight || a.getClientRects().length );
})(%s)`
visibleJS = `function visible() {
return Boolean( this.offsetWidth || this.offsetHeight || this.getClientRects().length );
}`

// waitForPredicatePageFunction is a javascript snippet that runs the polling in the
// browser. It's copied from puppeteer. See
Expand Down Expand Up @@ -168,34 +155,3 @@ const (
}
}`
)

// snippet builds a Javascript expression snippet.
func snippet(js string, f func(n *cdp.Node) string, sel interface{}, n *cdp.Node, v ...interface{}) string {
switch s := sel.(type) {
case *Selector:
if s != nil && s.raw {
return fmt.Sprintf(js, append([]interface{}{s.selAsString()}, v...)...)
}
}
return fmt.Sprintf(js, append([]interface{}{f(n)}, v...)...)
}

// cashX returns the $x() expression using the node's full xpath value.
func cashX(flatten bool) func(*cdp.Node) string {
return func(n *cdp.Node) string {
if flatten {
return fmt.Sprintf(`$x(%q)[0]`, n.PartialXPath())
}
return fmt.Sprintf(`$x(%q)`, n.PartialXPath())
}
}

// cashXNode returns the $x(/node()) expression using the node's full xpath value.
func cashXNode(flatten bool) func(*cdp.Node) string {
return func(n *cdp.Node) string {
if flatten {
return fmt.Sprintf(`$x(%q)[0]`, n.PartialXPath()+"/node()")
}
return fmt.Sprintf(`$x(%q)`, n.PartialXPath()+"/node()")
}
}
50 changes: 30 additions & 20 deletions query.go
Expand Up @@ -34,7 +34,6 @@ type Selector struct {
by func(context.Context, *cdp.Node) ([]cdp.NodeID, error)
wait func(context.Context, *cdp.Frame, runtime.ExecutionContextID, ...cdp.NodeID) ([]*cdp.Node, error)
after func(context.Context, runtime.ExecutionContextID, ...*cdp.Node) error
raw bool
}

// Query is a query action that queries the browser for specific element
Expand Down Expand Up @@ -358,7 +357,6 @@ func BySearch(s *Selector) {
// Note: Do not use with an untrusted selector value, as any defined selector
// will be passed to runtime.Evaluate.
func ByJSPath(s *Selector) {
s.raw = true
ByFunc(func(ctx context.Context, n *cdp.Node) ([]cdp.NodeID, error) {
// set up eval command
p := runtime.Evaluate(s.selAsString()).
Expand Down Expand Up @@ -425,15 +423,28 @@ func NodeReady(s *Selector) {
WaitFunc(s.waitReady(nil))(s)
}

func withContextID(id runtime.ExecutionContextID) EvaluateOption {
return func(p *runtime.EvaluateParams) *runtime.EvaluateParams {
return p.WithContextID(id)
func callFunctionOnNode(ctx context.Context, node *cdp.Node, function string, res interface{}, args ...interface{}) error {
r, err := dom.ResolveNode().WithNodeID(node.NodeID).Do(ctx)
if err != nil {
return err
}
}
err = CallFunctionOn(function, &res,
func(p *runtime.CallFunctionOnParams) *runtime.CallFunctionOnParams {
return p.WithObjectID(r.ObjectID)
},
args...,
).Do(ctx)

if err != nil {
return err
}

// Try to release the remote object.
// It will fail if the page is navigated or closed,
// and it's okay to ignore the error in this case.
_ = runtime.ReleaseObject(r.ObjectID).Do(ctx)

func evalInCtx(ctx context.Context, execCtx runtime.ExecutionContextID, expression string, res interface{}, opts ...EvaluateOption) error {
allOpts := append([]EvaluateOption{withContextID(execCtx)}, opts...)
return EvaluateAsDevTools(expression, &res, allOpts...).Do(ctx)
return nil
}

// NodeVisible is an element query option to wait until all queried element
Expand All @@ -452,7 +463,7 @@ func NodeVisible(s *Selector) {

// check visibility
var res bool
err = evalInCtx(ctx, execCtx, snippet(visibleJS, cashX(true), s, n), &res, withContextID(execCtx))
err = callFunctionOnNode(ctx, n, visibleJS, &res)
if err != nil {
return err
}
Expand All @@ -479,7 +490,7 @@ func NodeNotVisible(s *Selector) {

// check visibility
var res bool
err = evalInCtx(ctx, execCtx, snippet(visibleJS, cashX(true), s, n), &res)
err = callFunctionOnNode(ctx, n, visibleJS, &res)
if err != nil {
return err
}
Expand Down Expand Up @@ -648,7 +659,7 @@ func Blur(sel interface{}, opts ...QueryOption) QueryAction {
}

var res bool
err := evalInCtx(ctx, execCtx, snippet(blurJS, cashX(true), sel, nodes[0]), &res)
err := callFunctionOnNode(ctx, nodes[0], blurJS, &res)
if err != nil {
return err
}
Expand Down Expand Up @@ -689,7 +700,7 @@ func Text(sel interface{}, text *string, opts ...QueryOption) QueryAction {
return fmt.Errorf("selector %q did not return any nodes", sel)
}

return evalInCtx(ctx, execCtx, snippet(textJS, cashX(false), sel, nodes[0]), text)
return callFunctionOnNode(ctx, nodes[0], textJS, text)
}, opts...)
}

Expand All @@ -705,7 +716,7 @@ func TextContent(sel interface{}, text *string, opts ...QueryOption) QueryAction
return fmt.Errorf("selector %q did not return any nodes", sel)
}

return evalInCtx(ctx, execCtx, snippet(textContentJS, cashX(false), sel, nodes[0]), text)
return callFunctionOnNode(ctx, nodes[0], textContentJS, text)
}, opts...)
}

Expand Down Expand Up @@ -931,11 +942,10 @@ func JavascriptAttribute(sel interface{}, name string, res interface{}, opts ...
return fmt.Errorf("selector %q did not return any nodes", sel)
}

if err := evalInCtx(ctx, execCtx,
snippet(attributeJS, cashX(true), sel, nodes[0], name), res,
); err != nil {
if err := callFunctionOnNode(ctx, nodes[0], attributeJS, res, name); err != nil {
return fmt.Errorf("could not retrieve attribute %q: %w", name, err)
}

return nil
}, opts...)
}
Expand All @@ -949,7 +959,7 @@ func SetJavascriptAttribute(sel interface{}, name, value string, opts ...QueryOp
}

var res string
err := evalInCtx(ctx, execCtx, snippet(setAttributeJS, cashX(true), sel, nodes[0], name, value), &res)
err := callFunctionOnNode(ctx, nodes[0], setAttributeJS, &res, name, value)
if err != nil {
return err
}
Expand Down Expand Up @@ -1109,7 +1119,7 @@ func Submit(sel interface{}, opts ...QueryOption) QueryAction {
}

var res bool
err := evalInCtx(ctx, execCtx, snippet(submitJS, cashX(true), sel, nodes[0]), &res)
err := callFunctionOnNode(ctx, nodes[0], submitJS, &res)
if err != nil {
return err
}
Expand All @@ -1131,7 +1141,7 @@ func Reset(sel interface{}, opts ...QueryOption) QueryAction {
}

var res bool
err := evalInCtx(ctx, execCtx, snippet(resetJS, cashX(true), sel, nodes[0]), &res)
err := callFunctionOnNode(ctx, nodes[0], resetJS, &res)
if err != nil {
return err
}
Expand Down

0 comments on commit eb823ff

Please sign in to comment.