From 79db58dfd0522f9006bbc211223ec716b1e09561 Mon Sep 17 00:00:00 2001 From: Yad Smood Date: Wed, 5 Aug 2020 19:47:41 +0900 Subject: [PATCH] abstract eval with EvalOptions --- dev_helpers.go | 23 +++++------- element.go | 69 ++++++++++++++++------------------ element_test.go | 4 +- examples_test.go | 4 +- hijack.go | 4 +- lib/assets/generate/main.go | 3 +- lib/assets/js/README.md | 3 ++ lib/assets/js/main.go | 2 + page.go | 40 ++++++++++---------- page_test.go | 9 +++-- query.go | 75 +++++++++++++++++-------------------- query_test.go | 10 ++--- sugar.go | 24 ++++++------ utils.go | 53 +++++++++++++++++++++----- 14 files changed, 176 insertions(+), 147 deletions(-) create mode 100644 lib/assets/js/README.md diff --git a/dev_helpers.go b/dev_helpers.go index 07073f71..e36b9ce0 100644 --- a/dev_helpers.go +++ b/dev_helpers.go @@ -15,6 +15,7 @@ import ( "time" "github.com/go-rod/rod/lib/assets" + "github.com/go-rod/rod/lib/assets/js" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" @@ -84,22 +85,20 @@ func (p *Page) Overlay(left, top, width, height float64, msg string) (remove fun root := p.Root() id := kit.RandString(8) - js, jsArgs := jsHelper("overlay", Array{ + _, err := root.EvalWithOptions(jsHelper(js.Overlay, Array{ id, left, top, width, height, msg, - }) - _, err := root.Eval(true, "", js, jsArgs) + })) if err != nil { p.browser.traceLogErr(err) } remove = func() { - js, jsArgs := jsHelper("removeOverlay", Array{id}) - _, _ = root.Eval(true, "", js, jsArgs) + _, _ = root.EvalWithOptions(jsHelper(js.RemoveOverlay, Array{id})) } return @@ -116,18 +115,16 @@ func (p *Page) ExposeJSHelper() *Page { func (el *Element) Trace(msg string) (removeOverlay func()) { id := kit.RandString(8) - js, jsArgs := jsHelper("elementOverlay", Array{ + _, err := el.EvalWithOptions(jsHelper(js.ElementOverlay, Array{ id, msg, - }) - _, err := el.Eval(true, js, jsArgs) + })) if err != nil { el.page.browser.traceLogErr(err) } removeOverlay = func() { - js, jsArgs := jsHelper("removeOverlay", Array{id}) - _, _ = el.Eval(true, js, jsArgs) + _, _ = el.EvalWithOptions(jsHelper(js.RemoveOverlay, Array{id})) } return @@ -196,13 +193,11 @@ func defaultTraceLogErr(err error) { } func (m *Mouse) initMouseTracer() { - js, params := jsHelper("initMouseTracer", Array{m.id, assets.MousePointer}) - _, _ = m.page.Eval(true, "", js, params) + _, _ = m.page.EvalWithOptions(jsHelper(js.InitMouseTracer, Array{m.id, assets.MousePointer})) } func (m *Mouse) updateMouseTracer() bool { - js, jsArgs := jsHelper("updateMouseTracer", Array{m.id, m.x, m.y}) - res, err := m.page.Eval(true, "", js, jsArgs) + res, err := m.page.EvalWithOptions(jsHelper(js.UpdateMouseTracer, Array{m.id, m.x, m.y})) if err != nil { return true } diff --git a/element.go b/element.go index 40828a11..ad16d865 100644 --- a/element.go +++ b/element.go @@ -11,6 +11,7 @@ import ( "github.com/tidwall/gjson" + "github.com/go-rod/rod/lib/assets/js" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/kit" @@ -35,7 +36,7 @@ func (el *Element) Focus() error { return err } - _, err = el.Eval(true, `this.focus()`, nil) + _, err = el.Eval(`this.focus()`) return err } @@ -103,7 +104,7 @@ func (el *Element) Clickable() (bool, error) { return false, err } - scroll, err := el.page.Root().Eval(true, "", `{ x: window.scrollX, y: window.scrollY }`, nil) + scroll, err := el.page.Root().Eval(`{ x: window.scrollX, y: window.scrollY }`) if err != nil { return false, err } @@ -164,8 +165,7 @@ func (el *Element) SelectText(regex string) error { defer el.tryTrace("select text: " + regex)() el.page.browser.trySlowmotion() - js, jsArgs := jsHelper("selectText", Array{regex}) - _, err = el.Eval(true, js, jsArgs) + _, err = el.EvalWithOptions(jsHelper(js.SelectText, Array{regex})) return err } @@ -179,8 +179,7 @@ func (el *Element) SelectAllText() error { defer el.tryTrace("select all text")() el.page.browser.trySlowmotion() - js, jsArgs := jsHelper("selectAllText", nil) - _, err = el.Eval(true, js, jsArgs) + _, err = el.EvalWithOptions(jsHelper(js.SelectAllText, nil)) return err } @@ -203,14 +202,13 @@ func (el *Element) Input(text string) error { return err } - js, jsArgs := jsHelper("inputEvent", nil) - _, err = el.Eval(true, js, jsArgs) + _, err = el.EvalWithOptions(jsHelper(js.InputEvent, nil)) return err } // Blur is similar to the method Blur func (el *Element) Blur() error { - _, err := el.Eval(true, "this.blur()", nil) + _, err := el.Eval("this.blur()") return err } @@ -226,14 +224,13 @@ func (el *Element) Select(selectors []string) error { strings.Join(selectors, "; ")))() el.page.browser.trySlowmotion() - js, jsArgs := jsHelper("select", Array{selectors}) - _, err = el.Eval(true, js, jsArgs) + _, err = el.EvalWithOptions(jsHelper(js.Select, Array{selectors})) return err } // Matches checks if the element can be selected by the css selector func (el *Element) Matches(selector string) (bool, error) { - res, err := el.Eval(true, `s => this.matches(s)`, Array{selector}) + res, err := el.Eval(`s => this.matches(s)`, selector) if err != nil { return false, err } @@ -242,7 +239,7 @@ func (el *Element) Matches(selector string) (bool, error) { // Attribute is similar to the method Attribute func (el *Element) Attribute(name string) (*string, error) { - attr, err := el.Eval(true, "(n) => this.getAttribute(n)", Array{name}) + attr, err := el.Eval("(n) => this.getAttribute(n)", name) if err != nil { return nil, err } @@ -256,7 +253,7 @@ func (el *Element) Attribute(name string) (*string, error) { // Property is similar to the method Property func (el *Element) Property(name string) (proto.JSON, error) { - prop, err := el.Eval(true, "(n) => this[n]", Array{name}) + prop, err := el.Eval("(n) => this[n]", name) if err != nil { return proto.JSON{}, err } @@ -333,8 +330,7 @@ func (el *Element) Frame() *Page { // ContainsElement check if the target is equal or inside the element. func (el *Element) ContainsElement(target *Element) (bool, error) { - js, args := jsHelper("containsElement", Array{target.ObjectID}) - res, err := el.Eval(true, js, args) + res, err := el.EvalWithOptions(jsHelper(js.ContainsElement, Array{target.ObjectID})) if err != nil { return false, err } @@ -343,8 +339,7 @@ func (el *Element) ContainsElement(target *Element) (bool, error) { // Text doc is similar to the method MustText func (el *Element) Text() (string, error) { - js, jsArgs := jsHelper("text", nil) - str, err := el.Eval(true, js, jsArgs) + str, err := el.EvalWithOptions(jsHelper(js.Text, nil)) if err != nil { return "", err } @@ -353,7 +348,7 @@ func (el *Element) Text() (string, error) { // HTML doc is similar to the method MustHTML func (el *Element) HTML() (string, error) { - str, err := el.Eval(true, `this.outerHTML`, nil) + str, err := el.Eval(`this.outerHTML`) if err != nil { return "", err } @@ -362,8 +357,7 @@ func (el *Element) HTML() (string, error) { // Visible doc is similar to the method MustVisible func (el *Element) Visible() (bool, error) { - js, jsArgs := jsHelper("visible", nil) - res, err := el.Eval(true, js, jsArgs) + res, err := el.EvalWithOptions(jsHelper(js.Visible, nil)) if err != nil { return false, err } @@ -372,8 +366,7 @@ func (el *Element) Visible() (bool, error) { // WaitLoad for element like func (el *Element) WaitLoad() error { - js, jsArgs := jsHelper("waitLoad", nil) - _, err := el.Eval(true, js, jsArgs) + _, err := el.EvalWithOptions(jsHelper(js.WaitLoad, nil)) return err } @@ -412,9 +405,9 @@ func (el *Element) WaitStable(interval time.Duration) error { } // Wait doc is similar to the method MustWait -func (el *Element) Wait(js string, params Array) error { +func (el *Element) Wait(js string, params ...interface{}) error { return kit.Retry(el.ctx, el.sleeper, func() (bool, error) { - res, err := el.Eval(true, js, params) + res, err := el.Eval(js, params...) if err != nil { return true, err } @@ -429,14 +422,14 @@ func (el *Element) Wait(js string, params Array) error { // WaitVisible doc is similar to the method MustWaitVisible func (el *Element) WaitVisible() error { - js, jsArgs := jsHelper("visible", nil) - return el.Wait(js, jsArgs) + opts := jsHelper(js.Visible, nil) + return el.Wait(opts.JS, opts.JSArgs...) } // WaitInvisible doc is similar to the method MustWaitInvisible func (el *Element) WaitInvisible() error { - js, jsArgs := jsHelper("invisible", nil) - return el.Wait(js, jsArgs) + opts := jsHelper(js.Invisible, nil) + return el.Wait(opts.JS, opts.JSArgs...) } // CanvasToImage get image data of a canvas. @@ -444,9 +437,7 @@ func (el *Element) WaitInvisible() error { // The default quality is 0.92. // doc: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL func (el *Element) CanvasToImage(format string, quality float64) ([]byte, error) { - res, err := el.Eval(true, - `(format, quality) => this.toDataURL(format, quality)`, - Array{format, quality}) + res, err := el.Eval(`(format, quality) => this.toDataURL(format, quality)`, format, quality) if err != nil { return nil, err } @@ -457,8 +448,7 @@ func (el *Element) CanvasToImage(format string, quality float64) ([]byte, error) // Resource doc is similar to the method MustResource func (el *Element) Resource() ([]byte, error) { - js, jsArgs := jsHelper("resource", nil) - src, err := el.Eval(true, js, jsArgs) + src, err := el.EvalWithOptions(jsHelper(js.Resource, nil)) if err != nil { return nil, err } @@ -539,8 +529,13 @@ func (el *Element) CallContext() (context.Context, proto.Client, string) { } // Eval doc is similar to the method MustEval -func (el *Element) Eval(byValue bool, js string, params Array) (*proto.RuntimeRemoteObject, error) { - return el.page.Context(el.ctx, el.ctxCancel).Eval(byValue, el.ObjectID, js, params) +func (el *Element) Eval(js string, params ...interface{}) (*proto.RuntimeRemoteObject, error) { + return el.EvalWithOptions(NewEvalOptions(js, params)) +} + +// EvalWithOptions of Eval +func (el *Element) EvalWithOptions(opts *EvalOptions) (*proto.RuntimeRemoteObject, error) { + return el.page.Context(el.ctx, el.ctxCancel).EvalWithOptions(opts.This(el.ObjectID)) } func (el *Element) ensureParentPage(nodeID proto.DOMNodeID, objID proto.RuntimeRemoteObjectID) error { @@ -555,7 +550,7 @@ func (el *Element) ensureParentPage(nodeID proto.DOMNodeID, objID proto.RuntimeR // DFS for the iframe that holds the element var walk func(page *Page) error walk = func(page *Page) error { - list, err := page.Elements("", "iframe") + list, err := page.Elements("iframe") if err != nil { return err } diff --git a/element_test.go b/element_test.go index 1c5e6c48..8305c5f6 100644 --- a/element_test.go +++ b/element_test.go @@ -482,12 +482,12 @@ func (s *S) TestFnErr() { p := s.page.MustNavigate(srcFile("fixtures/click.html")) el := p.MustElement("button") - _, err := el.Eval(true, "foo()", nil) + _, err := el.Eval("foo()") s.Error(err) s.Contains(err.Error(), "ReferenceError: foo is not defined") s.True(errors.Is(err, rod.ErrEval)) - _, err = el.ElementByJS("foo()", nil) + _, err = el.ElementByJS(rod.NewEvalOptions("foo()", nil)) s.Error(err) s.Contains(err.Error(), "ReferenceError: foo is not defined") s.True(errors.Is(err, rod.ErrEval)) diff --git a/examples_test.go b/examples_test.go index 828927f1..924bb06f 100644 --- a/examples_test.go +++ b/examples_test.go @@ -199,11 +199,11 @@ func Example_customize_retry_strategy() { time.Sleep(time.Second / 2) return nil } - el, _ := page.Sleeper(sleeper).Element("", []string{"input"}) + el, _ := page.Sleeper(sleeper).Element("input") // If sleeper is nil page.ElementE will query without retrying. // If nothing found it will return an error. - el, err := page.Sleeper(nil).Element("", []string{"input"}) + el, err := page.Sleeper(nil).Element("input") if errors.Is(err, rod.ErrElementNotFound) { fmt.Println("element not found") } else if err != nil { diff --git a/hijack.go b/hijack.go index 65ca7a60..e6bc6deb 100644 --- a/hijack.go +++ b/hijack.go @@ -12,6 +12,7 @@ import ( "strings" "sync" + "github.com/go-rod/rod/lib/assets/js" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/tidwall/gjson" @@ -493,8 +494,7 @@ func (p *Page) GetDownloadFile(pattern string, resourceType proto.NetworkResourc u := downloading.URL if strings.HasPrefix(u, "blob:") { - js, params := jsHelper("fetchAsDataURL", Array{u}) - res, e := p.Eval(true, "", js, params) + res, e := p.EvalWithOptions(jsHelper(js.FetchAsDataURL, Array{u})) if e != nil { err = e wg.Done() diff --git a/lib/assets/generate/main.go b/lib/assets/generate/main.go index 2f18c944..75edfebd 100644 --- a/lib/assets/generate/main.go +++ b/lib/assets/generate/main.go @@ -86,7 +86,8 @@ func getDeviceList() string { func genHelperList(helper string) string { m := regexp.MustCompile(`\},?\n\n {4}(?:async )?([a-z][^ ]+) \(`).FindAllStringSubmatch(helper, -1) - list := "package js\n\n" + + list := "// generated by running \"go generate\" on project root\n\n" + + "package js\n\n" + "// NameType type\n" + "type NameType string\n\n" + "const (\n" diff --git a/lib/assets/js/README.md b/lib/assets/js/README.md new file mode 100644 index 00000000..2693bb35 --- /dev/null +++ b/lib/assets/js/README.md @@ -0,0 +1,3 @@ +# Overview + +The `main.go` lists all the function names from `../helper.js`. diff --git a/lib/assets/js/main.go b/lib/assets/js/main.go index 12545acc..c71f0c65 100644 --- a/lib/assets/js/main.go +++ b/lib/assets/js/main.go @@ -1,3 +1,5 @@ +// generated by running "go generate" on project root + package js // NameType type diff --git a/page.go b/page.go index 60f78225..61a79bbb 100644 --- a/page.go +++ b/page.go @@ -10,6 +10,7 @@ import ( "time" "github.com/go-rod/rod/lib/assets" + "github.com/go-rod/rod/lib/assets/js" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/devices" "github.com/go-rod/rod/lib/proto" @@ -407,15 +408,13 @@ func (p *Page) WaitRequestIdle(d time.Duration, includes, excludes []string) fun // WaitIdle doc is similar to the method MustWaitIdle func (p *Page) WaitIdle(timeout time.Duration) (err error) { - js, jsArgs := jsHelper("waitIdle", Array{timeout.Seconds()}) - _, err = p.Eval(true, "", js, jsArgs) + _, err = p.EvalWithOptions(jsHelper(js.WaitIdle, Array{timeout.Seconds()})) return err } // WaitLoad doc is similar to the method MustWaitLoad func (p *Page) WaitLoad() error { - js, jsArgs := jsHelper("waitLoad", nil) - _, err := p.Eval(true, "", js, jsArgs) + _, err := p.EvalWithOptions(jsHelper(js.WaitLoad, nil)) return err } @@ -423,8 +422,7 @@ func (p *Page) WaitLoad() error { func (p *Page) AddScriptTag(url, content string) error { hash := md5.Sum([]byte(url + content)) id := hex.EncodeToString(hash[:]) - js, jsArgs := jsHelper("addScriptTag", Array{id, url, content}) - _, err := p.Eval(true, "", js, jsArgs) + _, err := p.EvalWithOptions(jsHelper(js.AddScriptTag, Array{id, url, content})) return err } @@ -432,8 +430,7 @@ func (p *Page) AddScriptTag(url, content string) error { func (p *Page) AddStyleTag(url, content string) error { hash := md5.Sum([]byte(url + content)) id := hex.EncodeToString(hash[:]) - js, jsArgs := jsHelper("addStyleTag", Array{id, url, content}) - _, err := p.Eval(true, "", js, jsArgs) + _, err := p.EvalWithOptions(jsHelper(js.AddStyleTag, Array{id, url, content})) return err } @@ -475,18 +472,23 @@ func (p *Page) Expose(name string) (callback chan string, stop func(), err error return } -// Eval thisID is the remote objectID that will be the this of the js function, if it's empty "window" will be used. +// Eval evalutes javascript on the page. +func (p *Page) Eval(js string, jsArgs ...interface{}) (*proto.RuntimeRemoteObject, error) { + return p.EvalWithOptions(NewEvalOptions(js, jsArgs)) +} + +// EvalWithOptions thisID is the remote objectID that will be the this of the js function, if it's empty "window" will be used. // Set the byValue to true to reduce memory occupation. // If the item in jsArgs is proto.RuntimeRemoteObjectID, the remote object will be used, else the item will be treated as JSON value. -func (p *Page) Eval(byValue bool, thisID proto.RuntimeRemoteObjectID, js string, jsArgs Array) (*proto.RuntimeRemoteObject, error) { +func (p *Page) EvalWithOptions(opts *EvalOptions) (*proto.RuntimeRemoteObject, error) { backoff := kit.BackoffSleeper(30*time.Millisecond, 3*time.Second, nil) - objectID := thisID + objectID := opts.ThisID var err error var res *proto.RuntimeCallFunctionOnResult // js context will be invalid if a frame is reloaded err = kit.Retry(p.ctx, backoff, func() (bool, error) { - if p.getWindowObjectID() == "" || thisID == "" { + if p.getWindowObjectID() == "" || opts.ThisID == "" { err := p.initJS(false) if err != nil { if isNilContextErr(err) { @@ -495,12 +497,12 @@ func (p *Page) Eval(byValue bool, thisID proto.RuntimeRemoteObjectID, js string, return true, err } } - if thisID == "" { + if opts.ThisID == "" { objectID = p.getWindowObjectID() } args := []*proto.RuntimeCallArgument{} - for _, arg := range jsArgs { + for _, arg := range opts.JSArgs { if id, ok := arg.(proto.RuntimeRemoteObjectID); ok { if id == jsHelperID { id = p.getJSHelperObjectID() @@ -514,11 +516,11 @@ func (p *Page) Eval(byValue bool, thisID proto.RuntimeRemoteObjectID, js string, res, err = proto.RuntimeCallFunctionOn{ ObjectID: objectID, AwaitPromise: true, - ReturnByValue: byValue, - FunctionDeclaration: SprintFnThis(js), + ReturnByValue: opts.ByValue, + FunctionDeclaration: SprintFnThis(opts.JS), Arguments: args, }.Call(p) - if thisID == "" && isNilContextErr(err) { + if opts.ThisID == "" && isNilContextErr(err) { _ = p.initJS(true) return false, nil } @@ -555,7 +557,7 @@ func (p *Page) Wait(thisID proto.RuntimeRemoteObjectID, js string, params Array) removeTrace() removeTrace = remove - res, err := p.Eval(true, thisID, js, params) + res, err := p.EvalWithOptions(NewEvalOptions(js, params).This(thisID)) if err != nil { return true, err } @@ -792,7 +794,7 @@ func (p *Page) resolveNode(nodeID proto.DOMNodeID) (proto.RuntimeRemoteObjectID, func (p *Page) hasElement(id proto.RuntimeRemoteObjectID) (bool, error) { // We don't have a good way to detect if a node is inside an iframe. // Currently this is most efficient way to do it. - _, err := p.Eval(true, "", "() => {}", Array{id}) + _, err := p.Eval("() => {}", id) if err == nil { return true, nil } diff --git a/page_test.go b/page_test.go index 5f723054..d5f540e4 100644 --- a/page_test.go +++ b/page_test.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/go-rod/rod" "github.com/go-rod/rod/lib/devices" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/proto" @@ -111,7 +112,7 @@ func (s *S) TestPageContext() { } func (s *S) TestRelease() { - res, err := s.page.Eval(false, "", `document`, nil) + res, err := s.page.EvalWithOptions(rod.NewEvalOptions(`document`, nil).ByObject()) utils.E(err) s.page.MustRelease(res.ObjectID) } @@ -215,9 +216,9 @@ func (s *S) TestPageEvalOnNewDocument() { func (s *S) TestPageEval() { page := s.page.MustNavigate(srcFile("fixtures/click.html")) - s.EqualValues(1, page.MustEval(` - () => 1 - `).Int()) + s.EqualValues(3, page.MustEval(` + (a, b) => a + b + `, 1, 2).Int()) s.EqualValues(1, page.MustEval(`a => 1`).Int()) s.EqualValues(1, page.MustEval(`function() { return 1 }`).Int()) s.NotEqualValues(1, page.MustEval(`a = () => 1`).Int()) diff --git a/query.go b/query.go index 68a66ae7..798800b5 100644 --- a/query.go +++ b/query.go @@ -8,6 +8,7 @@ import ( "fmt" "regexp" + "github.com/go-rod/rod/lib/assets/js" "github.com/go-rod/rod/lib/proto" "github.com/ysmood/kit" ) @@ -52,7 +53,7 @@ func (ps Pages) Find(selector string) *Page { // FindByURL returns the page that has the url that matches the regex func (ps Pages) FindByURL(regex string) (*Page, error) { for _, page := range ps { - res, err := page.Eval(true, "", `location.href`, nil) + res, err := page.Eval(`location.href`) if err != nil { return nil, err } @@ -66,7 +67,7 @@ func (ps Pages) FindByURL(regex string) (*Page, error) { // Has doc is similar to the method MustHas func (p *Page) Has(selectors ...string) (bool, error) { - _, err := p.Sleeper(nil).Element("", selectors) + _, err := p.Sleeper(nil).Element(selectors...) if errors.Is(err, ErrElementNotFound) { return false, nil } @@ -75,7 +76,7 @@ func (p *Page) Has(selectors ...string) (bool, error) { // HasX doc is similar to the method MustHasX func (p *Page) HasX(selectors ...string) (bool, error) { - _, err := p.Sleeper(nil).ElementX("", selectors) + _, err := p.Sleeper(nil).ElementX(selectors...) if errors.Is(err, ErrElementNotFound) { return false, nil } @@ -84,7 +85,7 @@ func (p *Page) HasX(selectors ...string) (bool, error) { // HasMatches doc is similar to the method MustHasMatches func (p *Page) HasMatches(pairs ...string) (bool, error) { - _, err := p.Sleeper(nil).ElementMatches("", pairs) + _, err := p.Sleeper(nil).ElementMatches(pairs...) if errors.Is(err, ErrElementNotFound) { return false, nil } @@ -92,21 +93,18 @@ func (p *Page) HasMatches(pairs ...string) (bool, error) { } // Element finds element by css selector -func (p *Page) Element(objectID proto.RuntimeRemoteObjectID, selectors []string) (*Element, error) { - js, jsArgs := jsHelper("element", ArrayFromList(selectors)) - return p.ElementByJS(objectID, js, jsArgs) +func (p *Page) Element(selectors ...string) (*Element, error) { + return p.ElementByJS(jsHelper(js.Element, ArrayFromList(selectors))) } // ElementMatches doc is similar to the method MustElementMatches -func (p *Page) ElementMatches(objectID proto.RuntimeRemoteObjectID, pairs []string) (*Element, error) { - js, jsArgs := jsHelper("elementMatches", ArrayFromList(pairs)) - return p.ElementByJS(objectID, js, jsArgs) +func (p *Page) ElementMatches(pairs ...string) (*Element, error) { + return p.ElementByJS(jsHelper(js.ElementMatches, ArrayFromList(pairs))) } // ElementX finds elements by XPath -func (p *Page) ElementX(objectID proto.RuntimeRemoteObjectID, xPaths []string) (*Element, error) { - js, jsArgs := jsHelper("elementX", ArrayFromList(xPaths)) - return p.ElementByJS(objectID, js, jsArgs) +func (p *Page) ElementX(xPaths ...string) (*Element, error) { + return p.ElementByJS(jsHelper(js.ElementX, ArrayFromList(xPaths))) } // ElementByJS returns the element from the return value of the js function. @@ -114,24 +112,24 @@ func (p *Page) ElementX(objectID proto.RuntimeRemoteObjectID, xPaths []string) ( // thisID is the this value of the js function, when thisID is "", the this context will be the "window". // If the js function returns "null", ElementByJS will retry, you can use custom sleeper to make it only // retry once. -func (p *Page) ElementByJS(thisID proto.RuntimeRemoteObjectID, js string, params Array) (*Element, error) { +func (p *Page) ElementByJS(opts *EvalOptions) (*Element, error) { var res *proto.RuntimeRemoteObject var err error sleeper := p.sleeper if sleeper == nil { sleeper = func(_ context.Context) error { - return newErr(ErrElementNotFound, js, js) + return newErr(ErrElementNotFound, opts, opts.JS) } } removeTrace := func() {} err = kit.Retry(p.ctx, sleeper, func() (bool, error) { - remove := p.tryTraceFn(js, params) + remove := p.tryTraceFn(opts.JS, opts.JSArgs) removeTrace() removeTrace = remove - res, err = p.Eval(false, thisID, js, params) + res, err = p.EvalWithOptions(opts.ByObject()) if err != nil { return true, err } @@ -155,20 +153,18 @@ func (p *Page) ElementByJS(thisID proto.RuntimeRemoteObjectID, js string, params } // Elements doc is similar to the method MustElements -func (p *Page) Elements(objectID proto.RuntimeRemoteObjectID, selector string) (Elements, error) { - js, jsArgs := jsHelper("elements", Array{selector}) - return p.ElementsByJS(objectID, js, jsArgs) +func (p *Page) Elements(selector string) (Elements, error) { + return p.ElementsByJS(jsHelper(js.Elements, Array{selector})) } // ElementsX doc is similar to the method MustElementsX -func (p *Page) ElementsX(objectID proto.RuntimeRemoteObjectID, xpath string) (Elements, error) { - js, jsArgs := jsHelper("elementsX", Array{xpath}) - return p.ElementsByJS(objectID, js, jsArgs) +func (p *Page) ElementsX(xpath string) (Elements, error) { + return p.ElementsByJS(jsHelper(js.ElementsX, Array{xpath})) } // ElementsByJS is different from ElementByJSE, it doesn't do retry -func (p *Page) ElementsByJS(thisID proto.RuntimeRemoteObjectID, js string, params Array) (Elements, error) { - res, err := p.Eval(false, thisID, js, params) +func (p *Page) ElementsByJS(opts *EvalOptions) (Elements, error) { + res, err := p.EvalWithOptions(opts.ByObject()) if err != nil { return nil, err } @@ -208,7 +204,7 @@ func (p *Page) ElementsByJS(thisID proto.RuntimeRemoteObjectID, js string, param // Search for each given query in the DOM tree until the result count is not zero, before that it will keep retrying. // The query can be plain text or css selector or xpath. // It will search nested iframes and shadow doms too. -func (p *Page) Search(queries []string, from, to int) (Elements, error) { +func (p *Page) Search(from, to int, queries ...string) (Elements, error) { sleeper := p.sleeper if sleeper == nil { sleeper = func(_ context.Context) error { @@ -308,56 +304,55 @@ func (el *Element) HasMatches(selector, regex string) (bool, error) { // Element doc is similar to the method MustElement func (el *Element) Element(selectors ...string) (*Element, error) { - return el.page.Sleeper(nil).Element(el.ObjectID, selectors) + return el.ElementByJS(jsHelper(js.Element, ArrayFromList(selectors))) } // ElementX doc is similar to the method MustElementX func (el *Element) ElementX(xPaths ...string) (*Element, error) { - return el.page.Sleeper(nil).ElementX(el.ObjectID, xPaths) + return el.ElementByJS(jsHelper(js.ElementX, ArrayFromList(xPaths))) } // ElementByJS doc is similar to the method MustElementByJS -func (el *Element) ElementByJS(js string, params Array) (*Element, error) { - return el.page.Sleeper(nil).ElementByJS(el.ObjectID, js, params) +func (el *Element) ElementByJS(opts *EvalOptions) (*Element, error) { + return el.page.Sleeper(nil).ElementByJS(opts.This(el.ObjectID)) } // Parent doc is similar to the method MustParent func (el *Element) Parent() (*Element, error) { - return el.ElementByJS(`this.parentElement`, nil) + return el.ElementByJS(NewEvalOptions(`this.parentElement`, nil)) } // Parents that match the selector func (el *Element) Parents(selector string) (Elements, error) { - js, params := jsHelper("parents", Array{selector}) - return el.ElementsByJS(js, params) + return el.ElementsByJS(jsHelper(js.Parents, Array{selector})) } // Next doc is similar to the method MustNext func (el *Element) Next() (*Element, error) { - return el.ElementByJS(`this.nextElementSibling`, nil) + return el.ElementByJS(NewEvalOptions(`this.nextElementSibling`, nil)) } // Previous doc is similar to the method MustPrevious func (el *Element) Previous() (*Element, error) { - return el.ElementByJS(`this.previousElementSibling`, nil) + return el.ElementByJS(NewEvalOptions(`this.previousElementSibling`, nil)) } // ElementMatches doc is similar to the method MustElementMatches func (el *Element) ElementMatches(pairs ...string) (*Element, error) { - return el.page.Sleeper(nil).ElementMatches(el.ObjectID, pairs) + return el.ElementByJS(jsHelper(js.ElementMatches, ArrayFromList(pairs))) } // Elements doc is similar to the method MustElements func (el *Element) Elements(selector string) (Elements, error) { - return el.page.Elements(el.ObjectID, selector) + return el.ElementsByJS(jsHelper(js.Elements, Array{selector})) } // ElementsX doc is similar to the method MustElementsX func (el *Element) ElementsX(xpath string) (Elements, error) { - return el.page.ElementsX(el.ObjectID, xpath) + return el.ElementsByJS(jsHelper(js.ElementsX, Array{xpath})) } // ElementsByJS doc is similar to the method MustElementsByJS -func (el *Element) ElementsByJS(js string, params Array) (Elements, error) { - return el.page.ElementsByJS(el.ObjectID, js, params) +func (el *Element) ElementsByJS(opts *EvalOptions) (Elements, error) { + return el.page.Context(el.ctx, el.ctxCancel).Sleeper(nil).ElementsByJS(opts.This(el.ObjectID)) } diff --git a/query_test.go b/query_test.go index 155bd7d3..1c18e0bf 100644 --- a/query_test.go +++ b/query_test.go @@ -59,7 +59,7 @@ func (s *S) TestSearch() { s.Equal("click me", el.MustText()) s.True(el.MustClick().MustMatches("[a=ok]")) - _, err := p.Sleeper(nil).Search([]string{"not-exists"}, 0, 1) + _, err := p.Sleeper(nil).Search(0, 1, "not-exists") s.True(errors.Is(err, rod.ErrElementNotFound)) } @@ -167,17 +167,17 @@ func (s *S) TestElementTracing() { func (s *S) TestPageElementByJS_Err() { p := s.page.MustNavigate(srcFile("fixtures/click.html")) - _, err := p.ElementByJS("", `1`, nil) + _, err := p.ElementByJS(rod.NewEvalOptions(`1`, nil)) s.EqualError(err, `{"type":"number","value":1,"description":"1"}: expect js to return an element`) } func (s *S) TestPageElementsByJS_Err() { p := s.page.MustNavigate(srcFile("fixtures/click.html")) - _, err := p.ElementsByJS("", `[1]`, nil) + _, err := p.ElementsByJS(rod.NewEvalOptions(`[1]`, nil)) s.EqualError(err, `{"type":"number","value":1,"description":"1"}: expect js to return an array of elements`) - _, err = p.ElementsByJS("", `1`, nil) + _, err = p.ElementsByJS(rod.NewEvalOptions(`1`, nil)) s.EqualError(err, `{"type":"number","value":1,"description":"1"}: expect js to return an array of elements`) - _, err = p.ElementsByJS("", `foo()`, nil) + _, err = p.ElementsByJS(rod.NewEvalOptions(`foo()`, nil)) s.Error(err) } diff --git a/sugar.go b/sugar.go index 36e875ac..a1cd81fc 100644 --- a/sugar.go +++ b/sugar.go @@ -316,7 +316,7 @@ func (p *Page) MustExpose(name string) (callback chan string, stop func()) { // MustEval js on the page. The first param must be a js function definition. // For example page.MustEval(`n => n + 1`, 1) will return 2 func (p *Page) MustEval(js string, params ...interface{}) proto.JSON { - res, err := p.Eval(true, "", js, params) + res, err := p.Eval(js, params...) utils.E(err) return res.Value } @@ -391,14 +391,14 @@ func (p *Page) MustHasMatches(selector, regex string) bool { // The query can be plain text or css selector or xpath. // It will search nested iframes and shadow doms too. func (p *Page) MustSearch(queries ...string) *Element { - list, err := p.Search(queries, 0, 1) + list, err := p.Search(0, 1, queries...) utils.E(err) return list.First() } // MustElement retries until an element in the page that matches one of the CSS selectors func (p *Page) MustElement(selectors ...string) *Element { - el, err := p.Element("", selectors) + el, err := p.Element(selectors...) utils.E(err) return el } @@ -407,42 +407,42 @@ func (p *Page) MustElement(selectors ...string) *Element { // Each pairs is a css selector and a regex. A sample call will look like page.MustElementMatches("div", "click me"). // The regex is the js regex, not golang's. func (p *Page) MustElementMatches(pairs ...string) *Element { - el, err := p.ElementMatches("", pairs) + el, err := p.ElementMatches(pairs...) utils.E(err) return el } // MustElementByJS retries until returns the element from the return value of the js function func (p *Page) MustElementByJS(js string, params ...interface{}) *Element { - el, err := p.ElementByJS("", js, params) + el, err := p.ElementByJS(NewEvalOptions(js, params)) utils.E(err) return el } // MustElements returns all elements that match the css selector func (p *Page) MustElements(selector string) Elements { - list, err := p.Elements("", selector) + list, err := p.Elements(selector) utils.E(err) return list } // MustElementsX returns all elements that match the XPath selector func (p *Page) MustElementsX(xpath string) Elements { - list, err := p.ElementsX("", xpath) + list, err := p.ElementsX(xpath) utils.E(err) return list } // MustElementX retries until an element in the page that matches one of the XPath selectors func (p *Page) MustElementX(xPaths ...string) *Element { - el, err := p.ElementX("", xPaths) + el, err := p.ElementX(xPaths...) utils.E(err) return el } // MustElementsByJS returns the elements from the return value of the js func (p *Page) MustElementsByJS(js string, params ...interface{}) Elements { - list, err := p.ElementsByJS("", js, params) + list, err := p.ElementsByJS(NewEvalOptions(js, params)) utils.E(err) return list } @@ -723,7 +723,7 @@ func (el *Element) MustRelease() { // MustEval evaluates js function on the element, the first param must be a js function definition // For example: el.MustEval(`name => this.getAttribute(name)`, "value") func (el *Element) MustEval(js string, params ...interface{}) proto.JSON { - res, err := el.Eval(true, js, params) + res, err := el.Eval(js, params...) utils.E(err) return res.Value } @@ -765,7 +765,7 @@ func (el *Element) MustElementX(xpath string) *Element { // MustElementByJS returns the element from the return value of the js func (el *Element) MustElementByJS(js string, params ...interface{}) *Element { - el, err := el.ElementByJS(js, params) + el, err := el.ElementByJS(NewEvalOptions(js, params)) utils.E(err) return el } @@ -822,7 +822,7 @@ func (el *Element) MustElementsX(xpath string) Elements { // MustElementsByJS returns the elements from the return value of the js func (el *Element) MustElementsByJS(js string, params ...interface{}) Elements { - list, err := el.ElementsByJS(js, params) + list, err := el.ElementsByJS(NewEvalOptions(js, params)) utils.E(err) return list } diff --git a/utils.go b/utils.go index 48d43dcd..b4a9e457 100644 --- a/utils.go +++ b/utils.go @@ -12,6 +12,7 @@ import ( "regexp" "time" + "github.com/go-rod/rod/lib/assets/js" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" @@ -38,6 +39,49 @@ func ArrayFromList(list interface{}) Array { return arr } +// EvalOptions object +type EvalOptions struct { + // If enabled the eval will return an reference id for the + // remote object. If disabled the remote object will be return as json. + ByValue bool + + // ThisID is the this object when eval the js + ThisID proto.RuntimeRemoteObjectID + + // JS function code to eval + JS string + + // JSArgs of the js function + JSArgs Array +} + +// This set the ThisID +func (e *EvalOptions) This(id proto.RuntimeRemoteObjectID) *EvalOptions { + e.ThisID = id + return e +} + +// ByObject disables ByValue. +func (e *EvalOptions) ByObject() *EvalOptions { + e.ByValue = false + return e +} + +// NewEvalOptions creates a new EvalPayload +func NewEvalOptions(js string, jsArgs Array) *EvalOptions { + return &EvalOptions{true, "", js, jsArgs} +} + +const jsHelperID = proto.RuntimeRemoteObjectID("rodJSHelper") + +// Convert name and jsArgs to Page.Eval, the name is method name in the "lib/assets/helper.js". +func jsHelper(name js.Name, args Array) *EvalOptions { + return &EvalOptions{ + JSArgs: append(Array{jsHelperID}, args...), + JS: fmt.Sprintf(`(rod, ...args) => rod.%s.apply(this, args)`, name), + } +} + // SprintFnThis wrap js with this, wrap function call if it's js expression func SprintFnThis(js string) string { if detectJSFunction(js) { @@ -46,15 +90,6 @@ func SprintFnThis(js string) string { return fmt.Sprintf(`function() { return %s }`, js) } -const jsHelperID = proto.RuntimeRemoteObjectID("rodJSHelper") - -// Convert name and jsArgs to Page.Eval, the name is method name in the "lib/assets/helper.js". -func jsHelper(name string, jsArgs Array) (string, Array) { - jsArgs = append(Array{jsHelperID}, jsArgs...) - js := fmt.Sprintf(`(rod, ...args) => rod.%s.apply(this, args)`, name) - return js, jsArgs -} - // Event helps to convert a cdp.Event to proto.Payload. Returns false if the conversion fails func Event(msg *cdp.Event, evt proto.Payload) bool { if msg.Method == evt.MethodName() {