Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

convert javascript snippets to standalone embedded js files (part 1) #852

Merged
merged 5 commits into from Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
88 changes: 88 additions & 0 deletions call.go
@@ -0,0 +1,88 @@
package chromedp

import (
"context"
"encoding/json"

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

// CallAction are actions that calls a Javascript function using
// runtime.CallFunctionOn.
type CallAction Action

// CallFunctionOn is an action to call a Javascript function, unmarshaling
// the result of the function to res.
//
// The handling of res is the same as that of Evaluate.
//
// Do not call the following methods on runtime.CallFunctionOnParams:
// - WithReturnByValue: it will be set depending on the type of res;
// - WithArguments: pass the arguments with args instead.
//
// Note: any exception encountered will be returned as an error.
func CallFunctionOn(functionDeclaration string, res interface{}, opt CallOption, args ...interface{}) CallAction {
return ActionFunc(func(ctx context.Context) error {
// set up parameters
p := runtime.CallFunctionOn(functionDeclaration).
WithSilent(true)

switch res.(type) {
case nil, **runtime.RemoteObject:
default:
p = p.WithReturnByValue(true)
}

// apply opt
if opt != nil {
p = opt(p)
}

// arguments
if len(args) > 0 {
ea := &errAppender{args: make([]*runtime.CallArgument, 0, len(args))}
for _, arg := range args {
ea.append(arg)
}
if ea.err != nil {
return ea.err
}
p = p.WithArguments(ea.args)
}

// call
v, exp, err := p.Do(ctx)
if err != nil {
return err
}
if exp != nil {
return exp
}

return parseRemoteObject(v, res)
})
}

// CallOption is a function to modify the runtime.CallFunctionOnParams
// to provide more information.
type CallOption = func(params *runtime.CallFunctionOnParams) *runtime.CallFunctionOnParams

// errAppender is to help accumulating the arguments and simplifying error checks.
//
// see https://blog.golang.org/errors-are-values
type errAppender struct {
args []*runtime.CallArgument
err error
}

// append method calls the json.Marshal method to marshal the value and appends it to the slice.
// It records the first error for future reference.
// As soon as an error occurs, the append method becomes a no-op but the error value is saved.
func (ea *errAppender) append(v interface{}) {
if ea.err != nil {
return
}
var b []byte
b, ea.err = json.Marshal(v)
ea.args = append(ea.args, &runtime.CallArgument{Value: b})
}
69 changes: 39 additions & 30 deletions eval.go
Expand Up @@ -15,26 +15,31 @@ type EvaluateAction Action
// Evaluate is an action to evaluate the Javascript expression, unmarshaling
// the result of the script evaluation to res.
//
// When res is a type other than *[]byte, or **runtime.RemoteObject,
// then the result of the script evaluation will be returned "by value" (ie,
// When res is nil, the script result will be ignored.
//
// When res is a *[]byte, the raw JSON-encoded value of the script
// result will be placed in res.
//
// When res is a **runtime.RemoteObject, res will be set to the low-level
// protocol type, and no attempt will be made to convert the result.
// Original objects are maintained in memory until the page navigated or closed,
// unless they are either explicitly released or are released along with the
// other objects in their object group. runtime.ReleaseObject or
// runtime.ReleaseObjectGroup can be used to ask the browser to release
// original objects.
//
// For all other cases, the result of the script will be returned "by value" (ie,
// JSON-encoded), and subsequently an attempt will be made to json.Unmarshal
// the script result to res. It returns an error if the script result is
// "undefined" in this case.
//
// Otherwise, when res is a *[]byte, the raw JSON-encoded value of the script
// result will be placed in res. Similarly, if res is a **runtime.RemoteObject,
// then res will be set to the low-level protocol type, and no attempt will be
// made to convert the result. "undefined" is okay in this case.
//
// When res is nil, the script result will be ignored (including "undefined").
//
// Note: any exception encountered will be returned as an error.
func Evaluate(expression string, res interface{}, opts ...EvaluateOption) EvaluateAction {
return ActionFunc(func(ctx context.Context) error {
// set up parameters
p := runtime.Evaluate(expression)
switch res.(type) {
case nil, **runtime.RemoteObject:
case **runtime.RemoteObject:
default:
p = p.WithReturnByValue(true)
}
Expand All @@ -53,30 +58,34 @@ func Evaluate(expression string, res interface{}, opts ...EvaluateOption) Evalua
return exp
}

if res == nil {
return nil
}
return parseRemoteObject(v, res)
})
}

switch x := res.(type) {
case **runtime.RemoteObject:
*x = v
return nil
func parseRemoteObject(v *runtime.RemoteObject, res interface{}) error {
if res == nil {
return nil
}

case *[]byte:
*x = []byte(v.Value)
return nil
}
switch x := res.(type) {
case **runtime.RemoteObject:
*x = v
return nil

if v.Type == "undefined" {
// The unmarshal above would fail with the cryptic
// "unexpected end of JSON input" error, so try to give
// a better one here.
return fmt.Errorf("encountered an undefined value")
}
case *[]byte:
*x = v.Value
return nil
}

// unmarshal
return json.Unmarshal(v.Value, res)
})
if v.Type == "undefined" {
// The unmarshal below would fail with the cryptic
// "unexpected end of JSON input" error, so try to give
// a better one here.
return fmt.Errorf("encountered an undefined value")
}

// unmarshal
return json.Unmarshal(v.Value, res)
}

// EvaluateAsDevTools is an action that evaluates a Javascript expression as
Expand Down
8 changes: 1 addition & 7 deletions input.go
Expand Up @@ -60,14 +60,8 @@ func MouseClickNode(n *cdp.Node, opts ...MouseOption) MouseAction {
if t == nil {
return ErrInvalidTarget
}
frameID := t.enclosingFrame(n)
t.frameMu.RLock()
execCtx := t.execContexts[frameID]
t.frameMu.RUnlock()

var pos []float64
err := evalInCtx(ctx, execCtx, snippet(scrollIntoViewJS, cashX(true), nil, n), &pos)
if err != nil {
if err := dom.ScrollIntoViewIfNeeded().WithNodeID(n.NodeID).Do(ctx); err != nil {
return err
}

Expand Down
130 changes: 39 additions & 91 deletions js.go
@@ -1,97 +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)`

// scrollIntoViewJS is a javascript snippet that scrolls the specified node
// into the window's viewport (if needed), returning the actual window x/y
// after execution.
scrollIntoViewJS = `(function(a) {
a.scrollIntoViewIfNeeded(true);
return [window.scrollX, window.scrollY];
})(%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 @@ -176,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()")
}
}