2017-01-24 15:09:23 +00:00
|
|
|
package chromedp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
2017-12-27 02:30:28 +00:00
|
|
|
"github.com/chromedp/cdproto/cdp"
|
|
|
|
"github.com/chromedp/cdproto/dom"
|
2017-01-24 15:09:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
TODO: selector 'by' type, as below:
|
|
|
|
classname
|
|
|
|
linktext
|
|
|
|
name
|
|
|
|
partiallinktext
|
|
|
|
tagname
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Selector holds information pertaining to an element query select action.
|
|
|
|
type Selector struct {
|
|
|
|
sel interface{}
|
|
|
|
exp int
|
2019-03-15 17:17:57 +00:00
|
|
|
by func(context.Context, *Target, *cdp.Node) ([]cdp.NodeID, error)
|
|
|
|
wait func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error)
|
|
|
|
after func(context.Context, *Target, ...*cdp.Node) error
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Query is an action to query for document nodes match the specified sel and
|
|
|
|
// the supplied query options.
|
|
|
|
func Query(sel interface{}, opts ...QueryOption) Action {
|
|
|
|
s := &Selector{
|
|
|
|
sel: sel,
|
|
|
|
exp: 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
// apply options
|
|
|
|
for _, o := range opts {
|
|
|
|
o(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.by == nil {
|
|
|
|
BySearch(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.wait == nil {
|
2017-02-12 07:08:40 +00:00
|
|
|
NodeReady(s)
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do satisfies the Action interface.
|
2017-12-27 02:30:28 +00:00
|
|
|
func (s *Selector) Do(ctxt context.Context, h cdp.Executor) error {
|
2019-03-15 17:17:57 +00:00
|
|
|
th, ok := h.(*Target)
|
2017-12-27 02:30:28 +00:00
|
|
|
if !ok {
|
|
|
|
return ErrInvalidHandler
|
|
|
|
}
|
|
|
|
|
2017-01-24 15:09:23 +00:00
|
|
|
var err error
|
|
|
|
select {
|
2017-12-27 02:30:28 +00:00
|
|
|
case err = <-s.run(ctxt, th):
|
2017-01-24 15:09:23 +00:00
|
|
|
case <-ctxt.Done():
|
|
|
|
err = ctxt.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// run runs the selector action, starting over if the original returned nodes
|
|
|
|
// are invalidated prior to finishing the selector's by, wait, check, and after
|
|
|
|
// funcs.
|
2019-03-15 17:17:57 +00:00
|
|
|
func (s *Selector) run(ctxt context.Context, h *Target) chan error {
|
2019-02-21 16:58:08 +00:00
|
|
|
ch := make(chan error, 1)
|
2019-03-15 17:17:57 +00:00
|
|
|
h.waitQueue <- func(cur *cdp.Frame) bool {
|
|
|
|
cur.RLock()
|
|
|
|
root := cur.Root
|
|
|
|
cur.RUnlock()
|
|
|
|
|
|
|
|
if root == nil {
|
|
|
|
// not ready?
|
|
|
|
return false
|
|
|
|
}
|
2017-01-24 15:09:23 +00:00
|
|
|
|
2019-03-15 17:17:57 +00:00
|
|
|
ids, err := s.by(ctxt, h, root)
|
|
|
|
if err != nil || len(ids) < s.exp {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
nodes, err := s.wait(ctxt, h, cur, ids...)
|
|
|
|
// if nodes==nil, we're not yet ready
|
|
|
|
if nodes == nil || err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if s.after != nil {
|
|
|
|
if err := s.after(ctxt, h, nodes...); err != nil {
|
|
|
|
ch <- err
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
}
|
2019-03-15 17:17:57 +00:00
|
|
|
close(ch)
|
|
|
|
return true
|
|
|
|
}
|
2017-01-24 15:09:23 +00:00
|
|
|
return ch
|
|
|
|
}
|
|
|
|
|
|
|
|
// selAsString forces sel into a string.
|
|
|
|
func (s *Selector) selAsString() string {
|
|
|
|
if sel, ok := s.sel.(string); ok {
|
|
|
|
return sel
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("%s", s.sel)
|
|
|
|
}
|
|
|
|
|
|
|
|
// QueryAfter is an action that will match the specified sel using the supplied
|
|
|
|
// query options, and after the visibility conditions of the query have been
|
|
|
|
// met, will execute f.
|
2019-03-15 17:17:57 +00:00
|
|
|
func QueryAfter(sel interface{}, f func(context.Context, *Target, ...*cdp.Node) error, opts ...QueryOption) Action {
|
2017-01-24 15:09:23 +00:00
|
|
|
return Query(sel, append(opts, After(f))...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// QueryOption is a element query selector option.
|
|
|
|
type QueryOption func(*Selector)
|
|
|
|
|
|
|
|
// ByFunc is a query option to set the func used to select elements.
|
2019-03-15 17:17:57 +00:00
|
|
|
func ByFunc(f func(context.Context, *Target, *cdp.Node) ([]cdp.NodeID, error)) QueryOption {
|
2017-01-24 15:09:23 +00:00
|
|
|
return func(s *Selector) {
|
|
|
|
s.by = f
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ByQuery is a query option to select a single element using
|
|
|
|
// DOM.querySelector.
|
|
|
|
func ByQuery(s *Selector) {
|
2019-03-15 17:17:57 +00:00
|
|
|
ByFunc(func(ctxt context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
2017-01-24 15:09:23 +00:00
|
|
|
nodeID, err := dom.QuerySelector(n.NodeID, s.selAsString()).Do(ctxt, h)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-02-18 08:36:24 +00:00
|
|
|
if nodeID == cdp.EmptyNodeID {
|
2017-01-26 07:28:34 +00:00
|
|
|
return []cdp.NodeID{}, nil
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
|
2017-01-26 07:28:34 +00:00
|
|
|
return []cdp.NodeID{nodeID}, nil
|
2017-01-24 15:09:23 +00:00
|
|
|
})(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ByQueryAll is a query option to select elements by DOM.querySelectorAll.
|
|
|
|
func ByQueryAll(s *Selector) {
|
2019-03-15 17:17:57 +00:00
|
|
|
ByFunc(func(ctxt context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
2017-01-24 15:09:23 +00:00
|
|
|
return dom.QuerySelectorAll(n.NodeID, s.selAsString()).Do(ctxt, h)
|
|
|
|
})(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ByID is a query option to select a single element by their CSS #id.
|
|
|
|
func ByID(s *Selector) {
|
|
|
|
s.sel = "#" + strings.TrimPrefix(s.selAsString(), "#")
|
|
|
|
ByQuery(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// BySearch is a query option via DOM.performSearch (works with both CSS and
|
|
|
|
// XPath queries).
|
|
|
|
func BySearch(s *Selector) {
|
2019-03-15 17:17:57 +00:00
|
|
|
ByFunc(func(ctxt context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
2017-01-24 15:09:23 +00:00
|
|
|
id, count, err := dom.PerformSearch(s.selAsString()).Do(ctxt, h)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if count < 1 {
|
2017-01-26 07:28:34 +00:00
|
|
|
return []cdp.NodeID{}, nil
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
nodes, err := dom.GetSearchResults(id, 0, count).Do(ctxt, h)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nodes, nil
|
|
|
|
})(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ByNodeID is a query option to select elements by their NodeIDs.
|
|
|
|
func ByNodeID(s *Selector) {
|
2017-01-26 07:28:34 +00:00
|
|
|
ids, ok := s.sel.([]cdp.NodeID)
|
2017-01-24 15:09:23 +00:00
|
|
|
if !ok {
|
2017-01-26 07:28:34 +00:00
|
|
|
panic("ByNodeID can only work on []cdp.NodeID")
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
|
2019-03-15 17:17:57 +00:00
|
|
|
ByFunc(func(ctxt context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
2017-01-24 15:09:23 +00:00
|
|
|
for _, id := range ids {
|
2017-11-25 20:20:52 +00:00
|
|
|
err := dom.RequestChildNodes(id).WithPierce(true).Do(ctxt, h)
|
2017-01-24 15:09:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ids, nil
|
|
|
|
})(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// waitReady waits for the specified nodes to be ready.
|
2019-03-15 17:17:57 +00:00
|
|
|
func (s *Selector) waitReady(check func(context.Context, *Target, *cdp.Node) error) func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error) {
|
|
|
|
return func(ctxt context.Context, h *Target, cur *cdp.Frame, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
2017-01-26 07:28:34 +00:00
|
|
|
nodes := make([]*cdp.Node, len(ids))
|
2019-03-15 17:17:57 +00:00
|
|
|
cur.RLock()
|
2017-01-24 15:09:23 +00:00
|
|
|
for i, id := range ids {
|
2019-03-15 17:17:57 +00:00
|
|
|
nodes[i] = cur.Nodes[id]
|
|
|
|
if nodes[i] == nil {
|
|
|
|
cur.RUnlock()
|
|
|
|
// not yet ready
|
|
|
|
return nil, nil
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
}
|
2019-03-15 17:17:57 +00:00
|
|
|
cur.RUnlock()
|
2017-01-24 15:09:23 +00:00
|
|
|
|
|
|
|
if check != nil {
|
2019-03-15 17:17:57 +00:00
|
|
|
var wg sync.WaitGroup
|
2017-01-24 15:09:23 +00:00
|
|
|
errs := make([]error, len(nodes))
|
|
|
|
for i, n := range nodes {
|
|
|
|
wg.Add(1)
|
2017-01-26 07:28:34 +00:00
|
|
|
go func(i int, n *cdp.Node) {
|
2017-01-24 15:09:23 +00:00
|
|
|
defer wg.Done()
|
|
|
|
errs[i] = check(ctxt, h, n)
|
|
|
|
}(i, n)
|
|
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
for _, err := range errs {
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nodes, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WaitFunc is a query option to set a custom wait func.
|
2019-03-15 17:17:57 +00:00
|
|
|
func WaitFunc(wait func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error)) QueryOption {
|
2017-01-24 15:09:23 +00:00
|
|
|
return func(s *Selector) {
|
|
|
|
s.wait = wait
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-12 07:08:40 +00:00
|
|
|
// NodeReady is a query option to wait until the element is ready.
|
|
|
|
func NodeReady(s *Selector) {
|
2017-01-24 15:09:23 +00:00
|
|
|
WaitFunc(s.waitReady(nil))(s)
|
|
|
|
}
|
|
|
|
|
2017-02-12 07:08:40 +00:00
|
|
|
// NodeVisible is a query option to wait until the element is visible.
|
|
|
|
func NodeVisible(s *Selector) {
|
2019-03-15 17:17:57 +00:00
|
|
|
WaitFunc(s.waitReady(func(ctxt context.Context, h *Target, n *cdp.Node) error {
|
2017-02-08 16:01:35 +00:00
|
|
|
// check box model
|
2017-11-25 20:20:52 +00:00
|
|
|
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
2017-02-08 16:01:35 +00:00
|
|
|
if err != nil {
|
|
|
|
if isCouldNotComputeBoxModelError(err) {
|
|
|
|
return ErrNotVisible
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// check offsetParent
|
2017-02-08 08:40:22 +00:00
|
|
|
var res bool
|
2017-02-08 16:01:35 +00:00
|
|
|
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h)
|
2017-02-08 08:40:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !res {
|
|
|
|
return ErrNotVisible
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}))(s)
|
|
|
|
}
|
|
|
|
|
2017-02-12 07:08:40 +00:00
|
|
|
// NodeNotVisible is a query option to wait until the element is not visible.
|
|
|
|
func NodeNotVisible(s *Selector) {
|
2019-03-15 17:17:57 +00:00
|
|
|
WaitFunc(s.waitReady(func(ctxt context.Context, h *Target, n *cdp.Node) error {
|
2017-02-08 16:01:35 +00:00
|
|
|
// check box model
|
2017-11-25 20:20:52 +00:00
|
|
|
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
2017-02-08 16:01:35 +00:00
|
|
|
if err != nil {
|
|
|
|
if isCouldNotComputeBoxModelError(err) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// check offsetParent
|
2017-02-08 08:40:22 +00:00
|
|
|
var res bool
|
2017-02-08 16:01:35 +00:00
|
|
|
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h)
|
2017-02-08 08:40:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if res {
|
|
|
|
return ErrVisible
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}))(s)
|
|
|
|
}
|
|
|
|
|
2017-02-12 07:08:40 +00:00
|
|
|
// NodeEnabled is a query option to wait until the element is enabled.
|
|
|
|
func NodeEnabled(s *Selector) {
|
2019-03-15 17:17:57 +00:00
|
|
|
WaitFunc(s.waitReady(func(ctxt context.Context, h *Target, n *cdp.Node) error {
|
2017-01-24 15:09:23 +00:00
|
|
|
n.RLock()
|
|
|
|
defer n.RUnlock()
|
|
|
|
|
|
|
|
for i := 0; i < len(n.Attributes); i += 2 {
|
|
|
|
if n.Attributes[i] == "disabled" {
|
|
|
|
return ErrDisabled
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}))(s)
|
|
|
|
}
|
|
|
|
|
2017-02-12 07:08:40 +00:00
|
|
|
// NodeSelected is a query option to wait until the element is selected.
|
|
|
|
func NodeSelected(s *Selector) {
|
2019-03-15 17:17:57 +00:00
|
|
|
WaitFunc(s.waitReady(func(ctxt context.Context, h *Target, n *cdp.Node) error {
|
2017-01-24 15:09:23 +00:00
|
|
|
n.RLock()
|
|
|
|
defer n.RUnlock()
|
|
|
|
|
|
|
|
for i := 0; i < len(n.Attributes); i += 2 {
|
|
|
|
if n.Attributes[i] == "selected" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ErrNotSelected
|
|
|
|
}))(s)
|
|
|
|
}
|
|
|
|
|
2019-03-15 17:17:57 +00:00
|
|
|
// NodeNotPresent is a query option to wait until no elements are present
|
|
|
|
// matching the selector.
|
2017-02-12 07:08:40 +00:00
|
|
|
func NodeNotPresent(s *Selector) {
|
2017-02-08 16:16:28 +00:00
|
|
|
s.exp = 0
|
2019-03-15 17:17:57 +00:00
|
|
|
WaitFunc(func(ctxt context.Context, h *Target, cur *cdp.Frame, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
2017-02-08 16:16:28 +00:00
|
|
|
if len(ids) != 0 {
|
|
|
|
return nil, ErrHasResults
|
|
|
|
}
|
|
|
|
return []*cdp.Node{}, nil
|
|
|
|
})(s)
|
|
|
|
}
|
|
|
|
|
2017-01-24 15:09:23 +00:00
|
|
|
// AtLeast is a query option to wait until at least n elements are returned
|
|
|
|
// from the query selector.
|
|
|
|
func AtLeast(n int) QueryOption {
|
|
|
|
return func(s *Selector) {
|
|
|
|
s.exp = n
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// After is a query option to set a func that will be executed after the wait
|
|
|
|
// has succeeded.
|
2019-03-15 17:17:57 +00:00
|
|
|
func After(f func(context.Context, *Target, ...*cdp.Node) error) QueryOption {
|
2017-01-24 15:09:23 +00:00
|
|
|
return func(s *Selector) {
|
|
|
|
s.after = f
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WaitReady waits until the element is ready (ie, loaded by chromedp).
|
|
|
|
func WaitReady(sel interface{}, opts ...QueryOption) Action {
|
|
|
|
return Query(sel, opts...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WaitVisible waits until the selected element is visible.
|
|
|
|
func WaitVisible(sel interface{}, opts ...QueryOption) Action {
|
2017-02-12 07:08:40 +00:00
|
|
|
return Query(sel, append(opts, NodeVisible)...)
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// WaitNotVisible waits until the selected element is not visible.
|
|
|
|
func WaitNotVisible(sel interface{}, opts ...QueryOption) Action {
|
2017-02-12 07:08:40 +00:00
|
|
|
return Query(sel, append(opts, NodeNotVisible)...)
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// WaitEnabled waits until the selected element is enabled (does not have
|
|
|
|
// attribute 'disabled').
|
|
|
|
func WaitEnabled(sel interface{}, opts ...QueryOption) Action {
|
2017-02-12 07:08:40 +00:00
|
|
|
return Query(sel, append(opts, NodeEnabled)...)
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// WaitSelected waits until the element is selected (has attribute 'selected').
|
|
|
|
func WaitSelected(sel interface{}, opts ...QueryOption) Action {
|
2017-02-12 07:08:40 +00:00
|
|
|
return Query(sel, append(opts, NodeSelected)...)
|
2017-01-24 15:09:23 +00:00
|
|
|
}
|
2017-02-08 08:40:22 +00:00
|
|
|
|
2017-02-08 16:16:28 +00:00
|
|
|
// WaitNotPresent waits until no elements match the specified selector.
|
|
|
|
func WaitNotPresent(sel interface{}, opts ...QueryOption) Action {
|
2017-02-12 07:08:40 +00:00
|
|
|
return Query(sel, append(opts, NodeNotPresent)...)
|
2017-02-08 16:16:28 +00:00
|
|
|
}
|