From 6769aefc5e7d14b2a77c1450efc5b448f851e5da Mon Sep 17 00:00:00 2001 From: Kenneth Shaw Date: Wed, 8 Feb 2017 23:01:35 +0700 Subject: [PATCH] Fixing Visible/NotVisible issues; adding new actions - Fixed issues with ElementVisible/ElementNotVisible - Added a few extra Javascript based actions --- examples/submit/main.go | 17 ++-- input.go | 5 +- query.go | 187 +++++++++++++++++++++++++++------------- sel.go | 30 ++++++- util.go | 7 ++ 5 files changed, 174 insertions(+), 72 deletions(-) diff --git a/examples/submit/main.go b/examples/submit/main.go index 4357216..36f00af 100644 --- a/examples/submit/main.go +++ b/examples/submit/main.go @@ -3,7 +3,6 @@ package main import ( "context" "log" - "time" cdp "github.com/knq/chromedp" ) @@ -22,7 +21,8 @@ func main() { } // run task list - err = c.Run(ctxt, submit(`https://brank.as/`, `#outro-invite-form input[name="email"]`)) + var res string + err = c.Run(ctxt, submit(`https://github.com/search`, `//input[@name="q"]`, `chromedp`, &res)) if err != nil { log.Fatal(err) } @@ -38,15 +38,16 @@ func main() { if err != nil { log.Fatal(err) } + + log.Printf("got: `%s`", res) } -func submit(urlstr, sel string) cdp.Tasks { +func submit(urlstr, sel, q string, res *string) cdp.Tasks { return cdp.Tasks{ cdp.Navigate(urlstr), - cdp.Sleep(2 * time.Second), - cdp.WaitVisible(sel, cdp.ByQuery), - cdp.WaitNotVisible(`div.v-middle > div.la-ball-clip-rotate`, cdp.ByQuery), - cdp.Submit(sel, cdp.ElementVisible, cdp.ByQuery), - cdp.Sleep(120 * time.Second), + cdp.WaitVisible(sel), + cdp.SendKeys(sel, q), + cdp.Submit(sel), + cdp.Text(`//*[@id="js-pjax-container"]/div[2]/div/div[2]/ul/li/p`, res), } } diff --git a/input.go b/input.go index 6757faa..43233f6 100644 --- a/input.go +++ b/input.go @@ -55,8 +55,9 @@ func MouseClickXY(x, y int64, opts ...MouseOption) Action { }) } -// MouseActionNode dispatches a mouse event at the center of a specified node. -func MouseActionNode(n *cdp.Node, opts ...MouseOption) Action { +// MouseClickNode dispatches a mouse left button click event at the center of a +// specified node. +func MouseClickNode(n *cdp.Node, opts ...MouseOption) Action { return ActionFunc(func(ctxt context.Context, h cdp.FrameHandler) error { var err error diff --git a/query.go b/query.go index 8113c05..11034cf 100644 --- a/query.go +++ b/query.go @@ -22,6 +22,36 @@ var ( ErrInvalidBoxModel = errors.New("invalid box model") ) +// Nodes retrieves the DOM nodes matching the selector. +func Nodes(sel interface{}, nodes *[]*cdp.Node, opts ...QueryOption) Action { + if nodes == nil { + panic("nodes cannot be nil") + } + + return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, n ...*cdp.Node) error { + *nodes = n + return nil + }, opts...) +} + +// NodeIDs returns the node IDs of the matching selector. +func NodeIDs(sel interface{}, ids *[]cdp.NodeID, opts ...QueryOption) Action { + if ids == nil { + panic("nodes cannot be nil") + } + + return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { + nodeIDs := make([]cdp.NodeID, len(nodes)) + for i, n := range nodes { + nodeIDs[i] = n.NodeID + } + + *ids = nodeIDs + + return nil + }, opts...) +} + // Focus focuses the first element returned by the selector. func Focus(sel interface{}, opts ...QueryOption) Action { return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { @@ -33,6 +63,40 @@ func Focus(sel interface{}, opts ...QueryOption) Action { }, opts...) } +// Blur unfocuses (blurs) the first element returned by the selector. +func Blur(sel interface{}, opts ...QueryOption) Action { + return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { + if len(nodes) < 1 { + return fmt.Errorf("selector `%s` did not return any nodes", sel) + } + + var res bool + err := EvaluateAsDevTools(fmt.Sprintf(blurJS, nodes[0].FullXPath()), &res).Do(ctxt, h) + if err != nil { + return err + } + if !res { + return fmt.Errorf("could not blur node %d", nodes[0].NodeID) + } + return nil + }, opts...) +} + +// Text retrieves the text of the first element matching the selector. +func Text(sel interface{}, text *string, opts ...QueryOption) Action { + if text == nil { + panic("text cannot be nil") + } + + return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { + if len(nodes) < 1 { + return fmt.Errorf("selector `%s` did not return any nodes", sel) + } + + return EvaluateAsDevTools(fmt.Sprintf(textJS, nodes[0].FullXPath()), text).Do(ctxt, h) + }, opts...) +} + // Clear clears input and textarea fields of their values. func Clear(sel interface{}, opts ...QueryOption) Action { return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { @@ -74,36 +138,6 @@ func Clear(sel interface{}, opts ...QueryOption) Action { }, opts...) } -// Nodes retrieves the DOM nodes matching the selector. -func Nodes(sel interface{}, nodes *[]*cdp.Node, opts ...QueryOption) Action { - if nodes == nil { - panic("nodes cannot be nil") - } - - return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, n ...*cdp.Node) error { - *nodes = n - return nil - }, opts...) -} - -// NodeIDs returns the node IDs of the matching selector. -func NodeIDs(sel interface{}, ids *[]cdp.NodeID, opts ...QueryOption) Action { - if ids == nil { - panic("nodes cannot be nil") - } - - return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { - nodeIDs := make([]cdp.NodeID, len(nodes)) - for i, n := range nodes { - nodeIDs[i] = n.NodeID - } - - *ids = nodeIDs - - return nil - }, opts...) -} - // Dimensions retrieves the box model dimensions for the first node matching // the specified selector. func Dimensions(sel interface{}, model **dom.BoxModel, opts ...QueryOption) Action { @@ -155,21 +189,6 @@ func SetValue(sel interface{}, value string, opts ...QueryOption) Action { }, opts...) } -// Text retrieves the text of the first element matching the selector. -func Text(sel interface{}, text *string, opts ...QueryOption) Action { - if text == nil { - panic("text cannot be nil") - } - - return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { - if len(nodes) < 1 { - return fmt.Errorf("selector `%s` did not return any nodes", sel) - } - - return EvaluateAsDevTools(fmt.Sprintf(textJS, nodes[0].FullXPath()), text).Do(ctxt, h) - }, opts...) -} - // Attributes retrieves the attributes for the specified element. func Attributes(sel interface{}, attributes *map[string]string, opts ...QueryOption) Action { if attributes == nil { @@ -270,7 +289,7 @@ func Click(sel interface{}, opts ...QueryOption) Action { return fmt.Errorf("selector `%s` did not return any nodes", sel) } - return MouseActionNode(nodes[0], ClickCount(1)).Do(ctxt, h) + return MouseClickNode(nodes[0]).Do(ctxt, h) }, append(opts, ElementVisible)...) } @@ -281,21 +300,23 @@ func DoubleClick(sel interface{}, opts ...QueryOption) Action { return fmt.Errorf("selector `%s` did not return any nodes", sel) } - return MouseActionNode(nodes[0], ButtonLeft, ClickCount(2)).Do(ctxt, h) + return MouseClickNode(nodes[0], ClickCount(2)).Do(ctxt, h) }, append(opts, ElementVisible)...) } +// NOTE: temporarily disabling this until a proper unit test can be written. +// // Hover hovers (moves) the mouse over the first element returned by the // selector. -func Hover(sel interface{}, opts ...QueryOption) Action { - return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { - if len(nodes) < 1 { - return fmt.Errorf("selector `%s` did not return any nodes", sel) - } - - return MouseActionNode(nodes[0], ButtonNone).Do(ctxt, h) - }, append(opts, ElementVisible)...) -} +//func Hover(sel interface{}, opts ...QueryOption) Action { +// return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { +// if len(nodes) < 1 { +// return fmt.Errorf("selector `%s` did not return any nodes", sel) +// } +// +// return MouseClickNode(nodes[0], ButtonNone).Do(ctxt, h) +// }, append(opts, ElementVisible)...) +//} // SendKeys sends keys to the first element returned by selector. func SendKeys(sel interface{}, v string, opts ...QueryOption) Action { @@ -369,7 +390,8 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action { }, append(opts, ElementVisible)...) } -// Submit is an action that submits whatever form the first element belongs to. +// Submit is an action that submits whatever form the first element matching +// the selector belongs to. func Submit(sel interface{}, opts ...QueryOption) Action { return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { if len(nodes) < 1 { @@ -383,7 +405,29 @@ func Submit(sel interface{}, opts ...QueryOption) Action { } if !res { - return fmt.Errorf("submit on node %d returned false", nodes[0].NodeID) + return fmt.Errorf("could not call submit on node %d", nodes[0].NodeID) + } + + return nil + }, opts...) +} + +// Reset is an action that resets whatever form the first element matching the +// selector belongs to. +func Reset(sel interface{}, opts ...QueryOption) Action { + return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { + if len(nodes) < 1 { + return fmt.Errorf("selector `%s` did not return any nodes", sel) + } + + var res bool + err := EvaluateAsDevTools(fmt.Sprintf(resetJS, nodes[0].FullXPath()), &res).Do(ctxt, h) + if err != nil { + return err + } + + if !res { + return fmt.Errorf("could not call reset on node %d", nodes[0].NodeID) } return nil @@ -403,6 +447,12 @@ const ( return s; })($x("%s/node()"))` + // blurJS is a javscript snippet that blurs the specified element. + blurJS = `(function(a) { + a[0].blur(); + return true; + })($x('%s'))` + // scrollJS is a javascript snippet that scrolls the window to the // specified x, y coordinates and then returns the actual window x/y after // execution. @@ -414,8 +464,25 @@ const ( // 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[0].form !== null) { - return a[0].form.submit(); + if (a[0].nodeName === 'FORM') { + a[0].submit(); + return true; + } else if (a[0].form !== null) { + a[0].form.submit(); + return true; + } + return false; + })($x('%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[0].nodeName === 'FORM') { + a[0].reset(); + return true; + } else if (a[0].form !== null) { + a[0].form.reset(); + return true; } return false; })($x('%s'))` diff --git a/sel.go b/sel.go index 1b6cee3..967b37a 100644 --- a/sel.go +++ b/sel.go @@ -301,8 +301,21 @@ func ElementReady(s *Selector) { // ElementVisible is a query option to wait until the element is visible. func ElementVisible(s *Selector) { WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.FrameHandler, n *cdp.Node) error { + var err error + + // check box model + _, err = dom.GetBoxModel(n.NodeID).Do(ctxt, h) + if err != nil { + if isCouldNotComputeBoxModelError(err) { + return ErrNotVisible + } + + return err + } + + // check offsetParent var res bool - err := EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h) + err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h) if err != nil { return err } @@ -316,8 +329,21 @@ func ElementVisible(s *Selector) { // ElementNotVisible is a query option to wait until the element is not visible. func ElementNotVisible(s *Selector) { WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.FrameHandler, n *cdp.Node) error { + var err error + + // check box model + _, err = dom.GetBoxModel(n.NodeID).Do(ctxt, h) + if err != nil { + if isCouldNotComputeBoxModelError(err) { + return nil + } + + return err + } + + // check offsetParent var res bool - err := EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h) + err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h) if err != nil { return err } diff --git a/util.go b/util.go index b126b6a..5b727d6 100644 --- a/util.go +++ b/util.go @@ -260,3 +260,10 @@ func removeNode(n []*cdp.Node, id cdp.NodeID) []*cdp.Node { return append(n[:i], n[i+1:]...) } + +// isCouldNotComputeBoxModelError unwraps the err as a MessagError and +// determines if it is a compute box model error. +func isCouldNotComputeBoxModelError(err error) bool { + e, ok := err.(*cdp.MessageError) + return ok && e.Code == -32000 && e.Message == "Could not compute box model." +}