Fixing Visible/NotVisible issues; adding new actions

- Fixed issues with ElementVisible/ElementNotVisible
- Added a few extra Javascript based actions
This commit is contained in:
Kenneth Shaw 2017-02-08 23:01:35 +07:00
parent 705f3be8e5
commit 6769aefc5e
5 changed files with 174 additions and 72 deletions

View File

@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"log" "log"
"time"
cdp "github.com/knq/chromedp" cdp "github.com/knq/chromedp"
) )
@ -22,7 +21,8 @@ func main() {
} }
// run task list // 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -38,15 +38,16 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) 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{ return cdp.Tasks{
cdp.Navigate(urlstr), cdp.Navigate(urlstr),
cdp.Sleep(2 * time.Second), cdp.WaitVisible(sel),
cdp.WaitVisible(sel, cdp.ByQuery), cdp.SendKeys(sel, q),
cdp.WaitNotVisible(`div.v-middle > div.la-ball-clip-rotate`, cdp.ByQuery), cdp.Submit(sel),
cdp.Submit(sel, cdp.ElementVisible, cdp.ByQuery), cdp.Text(`//*[@id="js-pjax-container"]/div[2]/div/div[2]/ul/li/p`, res),
cdp.Sleep(120 * time.Second),
} }
} }

View File

@ -55,8 +55,9 @@ func MouseClickXY(x, y int64, opts ...MouseOption) Action {
}) })
} }
// MouseActionNode dispatches a mouse event at the center of a specified node. // MouseClickNode dispatches a mouse left button click event at the center of a
func MouseActionNode(n *cdp.Node, opts ...MouseOption) Action { // specified node.
func MouseClickNode(n *cdp.Node, opts ...MouseOption) Action {
return ActionFunc(func(ctxt context.Context, h cdp.FrameHandler) error { return ActionFunc(func(ctxt context.Context, h cdp.FrameHandler) error {
var err error var err error

187
query.go
View File

@ -22,6 +22,36 @@ var (
ErrInvalidBoxModel = errors.New("invalid box model") 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. // Focus focuses the first element returned by the selector.
func Focus(sel interface{}, opts ...QueryOption) Action { func Focus(sel interface{}, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { 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...) }, 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. // Clear clears input and textarea fields of their values.
func Clear(sel interface{}, opts ...QueryOption) Action { func Clear(sel interface{}, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { 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...) }, 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 // Dimensions retrieves the box model dimensions for the first node matching
// the specified selector. // the specified selector.
func Dimensions(sel interface{}, model **dom.BoxModel, opts ...QueryOption) Action { func Dimensions(sel interface{}, model **dom.BoxModel, opts ...QueryOption) Action {
@ -155,21 +189,6 @@ func SetValue(sel interface{}, value string, opts ...QueryOption) Action {
}, opts...) }, 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. // Attributes retrieves the attributes for the specified element.
func Attributes(sel interface{}, attributes *map[string]string, opts ...QueryOption) Action { func Attributes(sel interface{}, attributes *map[string]string, opts ...QueryOption) Action {
if attributes == nil { 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 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)...) }, 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 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)...) }, 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 // Hover hovers (moves) the mouse over the first element returned by the
// selector. // selector.
func Hover(sel interface{}, opts ...QueryOption) Action { //func Hover(sel interface{}, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { // return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { // if len(nodes) < 1 {
return fmt.Errorf("selector `%s` did not return any nodes", sel) // return fmt.Errorf("selector `%s` did not return any nodes", sel)
} // }
//
return MouseActionNode(nodes[0], ButtonNone).Do(ctxt, h) // return MouseClickNode(nodes[0], ButtonNone).Do(ctxt, h)
}, append(opts, ElementVisible)...) // }, append(opts, ElementVisible)...)
} //}
// SendKeys sends keys to the first element returned by selector. // SendKeys sends keys to the first element returned by selector.
func SendKeys(sel interface{}, v string, opts ...QueryOption) Action { 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)...) }, 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 { func Submit(sel interface{}, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
@ -383,7 +405,29 @@ func Submit(sel interface{}, opts ...QueryOption) Action {
} }
if !res { 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 return nil
@ -403,6 +447,12 @@ const (
return s; return s;
})($x("%s/node()"))` })($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 // scrollJS is a javascript snippet that scrolls the window to the
// specified x, y coordinates and then returns the actual window x/y after // specified x, y coordinates and then returns the actual window x/y after
// execution. // execution.
@ -414,8 +464,25 @@ const (
// submitJS is a javascript snippet that will call the containing form's // submitJS is a javascript snippet that will call the containing form's
// submit function, returning true or false if the call was successful. // submit function, returning true or false if the call was successful.
submitJS = `(function(a) { submitJS = `(function(a) {
if (a[0].form !== null) { if (a[0].nodeName === 'FORM') {
return a[0].form.submit(); 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; return false;
})($x('%s'))` })($x('%s'))`

30
sel.go
View File

@ -301,8 +301,21 @@ func ElementReady(s *Selector) {
// ElementVisible is a query option to wait until the element is visible. // ElementVisible is a query option to wait until the element is visible.
func ElementVisible(s *Selector) { func ElementVisible(s *Selector) {
WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.FrameHandler, n *cdp.Node) error { 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 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 { if err != nil {
return err return err
} }
@ -316,8 +329,21 @@ func ElementVisible(s *Selector) {
// ElementNotVisible is a query option to wait until the element is not visible. // ElementNotVisible is a query option to wait until the element is not visible.
func ElementNotVisible(s *Selector) { func ElementNotVisible(s *Selector) {
WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.FrameHandler, n *cdp.Node) error { 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 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 { if err != nil {
return err return err
} }

View File

@ -260,3 +260,10 @@ func removeNode(n []*cdp.Node, id cdp.NodeID) []*cdp.Node {
return append(n[:i], n[i+1:]...) 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."
}