Fixing Visible/NotVisible issues; adding new actions
- Fixed issues with ElementVisible/ElementNotVisible - Added a few extra Javascript based actions
This commit is contained in:
parent
705f3be8e5
commit
6769aefc5e
|
@ -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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
input.go
5
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.
|
// 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
187
query.go
|
@ -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
30
sel.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
7
util.go
7
util.go
|
@ -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."
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user