Code cleanup

- Refactored API calls to be cleaner
- Changed types that shoudn't be exported to not-exported
- Updated examples with API changes
- Added unit test for Title action
This commit is contained in:
Kenneth Shaw 2017-02-12 14:08:40 +07:00
parent f73c429109
commit 3673164aef
11 changed files with 283 additions and 316 deletions

View File

@ -54,9 +54,17 @@ func TestNavigate(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !strings.HasPrefix(urlstr, "https://www.google.") { if !strings.HasPrefix(urlstr, "https://www.google.") {
t.Errorf("expected to be on google, got: %v", urlstr) t.Errorf("expected to be on google domain, at: %s", urlstr)
}
var title string
err = c.Run(defaultContext, Title(&title))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(strings.ToLower(title), "google") {
t.Errorf("expected title to contain google, instead title is: %s", title)
} }
} }

39
errors.go Normal file
View File

@ -0,0 +1,39 @@
package chromedp
import (
"errors"
)
// Error types.
var (
// ErrInvalidDimensions is the error returned when the retrieved box model is
// invalid.
ErrInvalidDimensions = errors.New("invalid dimensions")
// ErrNoResults is the error returned when there are no matching nodes.
ErrNoResults = errors.New("no results")
// ErrHasResults is the error returned when there should not be any
// matching nodes.
ErrHasResults = errors.New("has results")
// ErrNotVisible is the error returned when a non-visible node should be
// visible.
ErrNotVisible = errors.New("not visible")
// ErrVisible is the error returned when a visible node should be
// non-visible.
ErrVisible = errors.New("visible")
// ErrDisabled is the error returned when a disabled node should be
// enabled.
ErrDisabled = errors.New("disabled")
// ErrNotSelected is the error returned when a non-selected node should be
// selected.
ErrNotSelected = errors.New("not selected")
// ErrInvalidBoxModel is the error returned when the retrieved box model
// data is invalid.
ErrInvalidBoxModel = errors.New("invalid box model")
)

View File

@ -44,7 +44,7 @@ func click() cdp.Tasks {
return cdp.Tasks{ return cdp.Tasks{
cdp.Navigate(`https://golang.org/pkg/time/`), cdp.Navigate(`https://golang.org/pkg/time/`),
cdp.WaitVisible(`#footer`), cdp.WaitVisible(`#footer`),
cdp.Click(`#pkg-overview`, cdp.ElementVisible), cdp.Click(`#pkg-overview`, cdp.NodeVisible),
cdp.Sleep(150 * time.Second), cdp.Sleep(150 * time.Second),
} }
} }

View File

@ -45,6 +45,6 @@ func main() {
func text(res *string) cdp.Tasks { func text(res *string) cdp.Tasks {
return cdp.Tasks{ return cdp.Tasks{
cdp.Navigate(`https://golang.org/pkg/time/`), cdp.Navigate(`https://golang.org/pkg/time/`),
cdp.Text(`#pkg-overview`, res, cdp.ElementVisible, cdp.ByID), cdp.Text(`#pkg-overview`, res, cdp.NodeVisible, cdp.ByID),
} }
} }

View File

@ -504,7 +504,7 @@ func (h *TargetHandler) pageEvent(ctxt context.Context, ev interface{}) {
defer h.pageWaitGroup.Done() defer h.pageWaitGroup.Done()
var id cdp.FrameID var id cdp.FrameID
var op FrameOp var op frameOp
switch e := ev.(type) { switch e := ev.(type) {
case *page.EventFrameNavigated: case *page.EventFrameNavigated:
@ -569,7 +569,7 @@ func (h *TargetHandler) domEvent(ctxt context.Context, ev interface{}) {
} }
var id cdp.NodeID var id cdp.NodeID
var op NodeOp var op nodeOp
switch e := ev.(type) { switch e := ev.(type) {
case *dom.EventSetChildNodes: case *dom.EventSetChildNodes:

View File

@ -2,7 +2,6 @@ package chromedp
import ( import (
"context" "context"
"errors"
"time" "time"
"github.com/knq/chromedp/cdp" "github.com/knq/chromedp/cdp"
@ -11,11 +10,6 @@ import (
"github.com/knq/chromedp/kb" "github.com/knq/chromedp/kb"
) )
// Error types.
var (
ErrInvalidDimensions = errors.New("invalid box dimensions")
)
// MouseAction is a mouse action. // MouseAction is a mouse action.
func MouseAction(typ input.MouseType, x, y int64, opts ...MouseOption) Action { func MouseAction(typ input.MouseType, x, y int64, opts ...MouseOption) Action {
me := input.DispatchMouseEvent(typ, x, y) me := input.DispatchMouseEvent(typ, x, y)

28
nav.go
View File

@ -82,6 +82,16 @@ func NavigateForward(ctxt context.Context, h cdp.Handler) error {
return page.NavigateToHistoryEntry(entries[i+1].ID).Do(ctxt, h) return page.NavigateToHistoryEntry(entries[i+1].ID).Do(ctxt, h)
} }
// Stop stops all navigation and pending resource retrieval.
func Stop() Action {
return page.StopLoading()
}
// Reload reloads the current page.
func Reload() Action {
return page.Reload()
}
// CaptureScreenshot captures takes a full page screenshot. // CaptureScreenshot captures takes a full page screenshot.
func CaptureScreenshot(res *[]byte) Action { func CaptureScreenshot(res *[]byte) Action {
if res == nil { if res == nil {
@ -113,16 +123,20 @@ func RemoveOnLoadScript(id page.ScriptIdentifier) Action {
return page.RemoveScriptToEvaluateOnLoad(id) return page.RemoveScriptToEvaluateOnLoad(id)
} }
// Stop stops all navigation and pending resource retrieval. // Location retrieves the document location.
func Stop() Action {
return page.StopLoading()
}
// Location retrieves the URL location.
func Location(urlstr *string) Action { func Location(urlstr *string) Action {
if urlstr == nil { if urlstr == nil {
panic("urlstr cannot be nil") panic("urlstr cannot be nil")
} }
return EvaluateAsDevTools(`location.toString()`, urlstr) return EvaluateAsDevTools(`document.location.toString()`, urlstr)
}
// Title retrieves the document title.
func Title(title *string) Action {
if title == nil {
panic("title cannot be nil")
}
return EvaluateAsDevTools(`document.title`, title)
} }

12
pool.go
View File

@ -8,14 +8,6 @@ import (
"github.com/knq/chromedp/runner" "github.com/knq/chromedp/runner"
) )
const (
// DefaultStartPort is the default start port number.
DefaultStartPort = 9000
// DefaultEndPort is the default end port number.
DefaultEndPort = 10000
)
// Pool manages a pool of running Chrome processes. // Pool manages a pool of running Chrome processes.
type Pool struct { type Pool struct {
// start is the start port. // start is the start port.
@ -35,8 +27,8 @@ func NewPool(opts ...PoolOption) (*Pool, error) {
var err error var err error
p := &Pool{ p := &Pool{
start: DefaultStartPort, start: DefaultPoolStartPort,
end: DefaultEndPort, end: DefaultPoolEndPort,
res: make(map[int]*Res), res: make(map[int]*Res),
} }

225
query.go
View File

@ -12,17 +12,12 @@ import (
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/knq/chromedp/cdp" "github.com/knq/chromedp/cdp"
"github.com/knq/chromedp/cdp/css"
"github.com/knq/chromedp/cdp/dom" "github.com/knq/chromedp/cdp/dom"
"github.com/knq/chromedp/cdp/page" "github.com/knq/chromedp/cdp/page"
) )
var ( // Nodes retrieves the document nodes matching the selector.
// ErrInvalidBoxModel is the error returned when the retrieved box model is
// invalid.
ErrInvalidBoxModel = errors.New("invalid box model")
)
// Nodes retrieves the DOM nodes matching the selector.
func Nodes(sel interface{}, nodes *[]*cdp.Node, opts ...QueryOption) Action { func Nodes(sel interface{}, nodes *[]*cdp.Node, opts ...QueryOption) Action {
if nodes == nil { if nodes == nil {
panic("nodes cannot be nil") panic("nodes cannot be nil")
@ -34,7 +29,7 @@ func Nodes(sel interface{}, nodes *[]*cdp.Node, opts ...QueryOption) Action {
}, opts...) }, opts...)
} }
// NodeIDs returns the node IDs of the matching selector. // NodeIDs retrieves the node IDs matching the selector.
func NodeIDs(sel interface{}, ids *[]cdp.NodeID, opts ...QueryOption) Action { func NodeIDs(sel interface{}, ids *[]cdp.NodeID, opts ...QueryOption) Action {
if ids == nil { if ids == nil {
panic("nodes cannot be nil") panic("nodes cannot be nil")
@ -52,7 +47,7 @@ func NodeIDs(sel interface{}, ids *[]cdp.NodeID, opts ...QueryOption) Action {
}, opts...) }, opts...)
} }
// Focus focuses the first element returned by the selector. // Focus focuses the first node matching 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.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
@ -63,7 +58,7 @@ func Focus(sel interface{}, opts ...QueryOption) Action {
}, opts...) }, opts...)
} }
// Blur unfocuses (blurs) the first element returned by the selector. // Blur unfocuses (blurs) the first node matching the selector.
func Blur(sel interface{}, opts ...QueryOption) Action { func Blur(sel interface{}, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
@ -75,14 +70,32 @@ func Blur(sel interface{}, opts ...QueryOption) Action {
if err != nil { if err != nil {
return err return err
} }
if !res { if !res {
return fmt.Errorf("could not blur node %d", nodes[0].NodeID) return fmt.Errorf("could not blur node %d", nodes[0].NodeID)
} }
return nil return nil
}, opts...) }, opts...)
} }
// Text retrieves the text of the first element matching the selector. // Dimensions retrieves the box model dimensions for the first node matching
// the selector.
func Dimensions(sel interface{}, model **dom.BoxModel, opts ...QueryOption) Action {
if model == nil {
panic("model cannot be nil")
}
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 {
return fmt.Errorf("selector `%s` did not return any nodes", sel)
}
var err error
*model, err = dom.GetBoxModel(nodes[0].NodeID).Do(ctxt, h)
return err
}, opts...)
}
// Text retrieves the visible text of the first node matching the selector.
func Text(sel interface{}, text *string, opts ...QueryOption) Action { func Text(sel interface{}, text *string, opts ...QueryOption) Action {
if text == nil { if text == nil {
panic("text cannot be nil") panic("text cannot be nil")
@ -97,7 +110,7 @@ func Text(sel interface{}, text *string, opts ...QueryOption) Action {
}, opts...) }, opts...)
} }
// Clear clears input and textarea fields of their values. // Clear clears the values of any input/textarea nodes matching the selector.
func Clear(sel interface{}, opts ...QueryOption) Action { func Clear(sel interface{}, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
@ -138,23 +151,7 @@ func Clear(sel interface{}, opts ...QueryOption) Action {
}, opts...) }, opts...)
} }
// Dimensions retrieves the box model dimensions for the first node matching // Value retrieves the value of the first node matching the selector.
// the specified selector.
func Dimensions(sel interface{}, model **dom.BoxModel, opts ...QueryOption) Action {
if model == nil {
panic("model cannot be nil")
}
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 {
return fmt.Errorf("selector `%s` did not return any nodes", sel)
}
var err error
*model, err = dom.GetBoxModel(nodes[0].NodeID).Do(ctxt, h)
return err
}, opts...)
}
// Value retrieves the value of an element.
func Value(sel interface{}, value *string, opts ...QueryOption) Action { func Value(sel interface{}, value *string, opts ...QueryOption) Action {
if value == nil { if value == nil {
panic("value cannot be nil") panic("value cannot be nil")
@ -189,7 +186,7 @@ func SetValue(sel interface{}, value string, opts ...QueryOption) Action {
}, opts...) }, opts...)
} }
// Attributes retrieves the attributes for the specified element. // Attributes retrieves the attributes for the first node matching the selector.
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 {
panic("attributes cannot be nil") panic("attributes cannot be nil")
@ -215,19 +212,26 @@ func Attributes(sel interface{}, attributes *map[string]string, opts ...QueryOpt
}, opts...) }, opts...)
} }
// SetAttributes sets the attributes for the specified element. // SetAttributes sets the attributes for the first node matching the selector.
func SetAttributes(sel interface{}, attributes map[string]string, opts ...QueryOption) Action { func SetAttributes(sel interface{}, attributes map[string]string, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
return errors.New("expected at least one element") return errors.New("expected at least one element")
} }
return nil attrs := make([]string, len(attributes)*2)
i := 0
for k, v := range attributes {
attrs[i], attrs[i+1] = k, v
i += 2
}
return dom.SetAttributesAsText(nodes[0].NodeID, strings.Join(attrs, " ")).Do(ctxt, h)
}, opts...) }, opts...)
} }
// AttributeValue retrieves the name'd attribute value for the specified // AttributeValue retrieves the attribute value for the first node matching the
// element. // selector.
func AttributeValue(sel interface{}, name string, value *string, ok *bool, opts ...QueryOption) Action { func AttributeValue(sel interface{}, name string, value *string, ok *bool, opts ...QueryOption) Action {
if value == nil { if value == nil {
panic("value cannot be nil") panic("value cannot be nil")
@ -260,7 +264,8 @@ func AttributeValue(sel interface{}, name string, value *string, ok *bool, opts
}, opts...) }, opts...)
} }
// SetAttributeValue sets an element's attribute with name to value. // SetAttributeValue sets the attribute with name to value on the first node
// matching the selector.
func SetAttributeValue(sel interface{}, name, value string, opts ...QueryOption) Action { func SetAttributeValue(sel interface{}, name, value string, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
@ -271,7 +276,8 @@ func SetAttributeValue(sel interface{}, name, value string, opts ...QueryOption)
}, opts...) }, opts...)
} }
// RemoveAttribute removes an element's attribute with name. // RemoveAttribute removes the attribute with name from the first node matching
// the selector.
func RemoveAttribute(sel interface{}, name string, opts ...QueryOption) Action { func RemoveAttribute(sel interface{}, name string, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
@ -282,7 +288,7 @@ func RemoveAttribute(sel interface{}, name string, opts ...QueryOption) Action {
}, opts...) }, opts...)
} }
// Click sends a click to the first element returned by the selector. // Click sends a mouse click event to the first node matching the selector.
func Click(sel interface{}, opts ...QueryOption) Action { func Click(sel interface{}, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
@ -290,10 +296,11 @@ func Click(sel interface{}, opts ...QueryOption) Action {
} }
return MouseClickNode(nodes[0]).Do(ctxt, h) return MouseClickNode(nodes[0]).Do(ctxt, h)
}, append(opts, ElementVisible)...) }, append(opts, NodeVisible)...)
} }
// DoubleClick does a double click on the first element returned by selector. // DoubleClick sends a mouse double click event to the first node matching the
// selector.
func DoubleClick(sel interface{}, opts ...QueryOption) Action { func DoubleClick(sel interface{}, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
@ -301,34 +308,21 @@ func DoubleClick(sel interface{}, opts ...QueryOption) Action {
} }
return MouseClickNode(nodes[0], ClickCount(2)).Do(ctxt, h) return MouseClickNode(nodes[0], ClickCount(2)).Do(ctxt, h)
}, append(opts, ElementVisible)...) }, append(opts, NodeVisible)...)
} }
// NOTE: temporarily disabling this until a proper unit test can be written. // SendKeys synthesizes the key up, char, and down events as needed for the
// // runes in v, sending them to the first node matching the selector.
// 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.Handler, 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 { func SendKeys(sel interface{}, v string, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, 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 KeyActionNode(nodes[0], v).Do(ctxt, h) return KeyActionNode(nodes[0], v).Do(ctxt, h)
}, append(opts, ElementVisible)...) }, append(opts, NodeVisible)...)
} }
// Screenshot takes a screenshot of the first element matching the selector. // Screenshot takes a screenshot of the first node matching the selector.
func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action { func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
if picbuf == nil { if picbuf == nil {
panic("picbuf cannot be nil") panic("picbuf cannot be nil")
@ -387,11 +381,11 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
*picbuf = croppedBuf.Bytes() *picbuf = croppedBuf.Bytes()
return nil return nil
}, append(opts, ElementVisible)...) }, append(opts, NodeVisible)...)
} }
// Submit is an action that submits whatever form the first element matching // Submit is an action that submits the form of the first node matching the
// the selector belongs to. // 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.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 { if len(nodes) < 1 {
@ -412,7 +406,7 @@ func Submit(sel interface{}, opts ...QueryOption) Action {
}, opts...) }, opts...)
} }
// Reset is an action that resets whatever form the first element matching the // Reset is an action that resets the form of the first node matching the
// selector belongs to. // selector belongs to.
func Reset(sel interface{}, opts ...QueryOption) Action { func Reset(sel interface{}, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
@ -434,84 +428,51 @@ func Reset(sel interface{}, opts ...QueryOption) Action {
}, opts...) }, opts...)
} }
const ( // ComputedStyle retrieves the computed style of the first node matching the selector.
// textJS is a javascript snippet that returns the concatenated textContent func ComputedStyle(sel interface{}, style *[]*css.ComputedProperty, opts ...QueryOption) Action {
// of all visible (ie, offsetParent !== null) children. if style == nil {
textJS = `(function(a) { panic("style cannot be nil")
var s = '';
for (var i = 0; i < a.length; i++) {
if (a[i].offsetParent !== null) {
s += a[i].textContent;
} }
return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
if len(nodes) < 1 {
return fmt.Errorf("selector `%s` did not return any nodes", sel)
} }
return s;
})($x("%s/node()"))`
// blurJS is a javscript snippet that blurs the specified element. computed, err := css.GetComputedStyleForNode(nodes[0].NodeID).Do(ctxt, h)
blurJS = `(function(a) { if err != nil {
a[0].blur(); return err
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.
scrollJS = `(function(x, y) {
window.scrollTo(x, y);
return [window.scrollX, window.scrollY];
})(%d, %d)`
// 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].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 *style = computed
// reset function, returning true or false if the call was successful.
resetJS = `(function(a) { return nil
if (a[0].nodeName === 'FORM') { }, opts...)
a[0].reset(); }
return true;
} else if (a[0].form !== null) { // MatchedStyle retrieves the matched style information for the first node
a[0].form.reset(); // matching the selector.
return true; func MatchedStyle(sel interface{}, style **css.GetMatchedStylesForNodeReturns, opts ...QueryOption) Action {
if style == nil {
panic("style cannot be nil")
} }
return false;
})($x('%s'))`
// valueJS is a javascript snippet that returns the value of a specified return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error {
// node. if len(nodes) < 1 {
valueJS = `(function(a) { return fmt.Errorf("selector `%s` did not return any nodes", sel)
return a[0].value; }
})($x('%s'))`
// setValueJS is a javascript snippet that sets the value of the specified var err error
// node, and returns the value. ret := &css.GetMatchedStylesForNodeReturns{}
setValueJS = `(function(a, val) { ret.InlineStyle, ret.AttributesStyle, ret.MatchedCSSRules,
return a[0].value = val; ret.PseudoElements, ret.Inherited, ret.CSSKeyframesRules,
})($x('%s'), '%s')` err = css.GetMatchedStylesForNode(nodes[0].NodeID).Do(ctxt, h)
) if err != nil {
return err
}
/* *style = ret
Title return nil
SetTitle }, opts...)
OuterHTML }
SetOuterHTML
NodeName -- ?
Style(Matched)
Style(Computed)
SetStyle
GetStyle(Inline)
*/

153
sel.go
View File

@ -2,14 +2,12 @@ package chromedp
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/knq/chromedp/cdp" "github.com/knq/chromedp/cdp"
"github.com/knq/chromedp/cdp/css"
"github.com/knq/chromedp/cdp/dom" "github.com/knq/chromedp/cdp/dom"
) )
@ -24,16 +22,6 @@ tagname
*/ */
// Error types.
var (
ErrNoResults = errors.New("no results")
ErrHasResults = errors.New("has results")
ErrNotVisible = errors.New("not visible")
ErrVisible = errors.New("visible")
ErrDisabled = errors.New("disabled")
ErrNotSelected = errors.New("not selected")
)
// Selector holds information pertaining to an element query select action. // Selector holds information pertaining to an element query select action.
type Selector struct { type Selector struct {
sel interface{} sel interface{}
@ -61,7 +49,7 @@ func Query(sel interface{}, opts ...QueryOption) Action {
} }
if s.wait == nil { if s.wait == nil {
ElementReady(s) NodeReady(s)
} }
return s return s
@ -294,13 +282,13 @@ func WaitFunc(wait func(context.Context, cdp.Handler, *cdp.Node, ...cdp.NodeID)
} }
} }
// ElementReady is a query option to wait until the element is ready. // NodeReady is a query option to wait until the element is ready.
func ElementReady(s *Selector) { func NodeReady(s *Selector) {
WaitFunc(s.waitReady(nil))(s) WaitFunc(s.waitReady(nil))(s)
} }
// ElementVisible is a query option to wait until the element is visible. // NodeVisible is a query option to wait until the element is visible.
func ElementVisible(s *Selector) { func NodeVisible(s *Selector) {
WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error { WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error {
var err error var err error
@ -327,8 +315,8 @@ func ElementVisible(s *Selector) {
}))(s) }))(s)
} }
// ElementNotVisible is a query option to wait until the element is not visible. // NodeNotVisible is a query option to wait until the element is not visible.
func ElementNotVisible(s *Selector) { func NodeNotVisible(s *Selector) {
WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error { WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error {
var err error var err error
@ -355,105 +343,8 @@ func ElementNotVisible(s *Selector) {
}))(s) }))(s)
} }
// ElementVisibleOld is a query option to wait until the element is visible. // NodeEnabled is a query option to wait until the element is enabled.
// func NodeEnabled(s *Selector) {
// This is the old, complicated, implementation (deprecated).
func ElementVisibleOld(s *Selector) {
WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error {
var err error
// check node has box model
_, err = dom.GetBoxModel(n.NodeID).Do(ctxt, h)
if err != nil {
return err
}
// check if any of the parents are not visible ...
var hidden bool
for ; n.Parent != nil; n = n.Parent {
// get style
style, err := css.GetComputedStyleForNode(n.NodeID).Do(ctxt, h)
if err != nil {
return err
}
// check if hidden
for _, c := range style {
switch c.Name {
case "display":
//log.Printf("%d >>>> %s=%s", n.NodeID, c.Name, c.Value)
hidden = c.Value == "none"
case "visibility":
//log.Printf("%d >>>> %s=%s", n.NodeID, c.Name, c.Value)
hidden = c.Value != "visible"
case "hidden":
//log.Printf("%d >>>> %s=%s", n.NodeID, c.Name, c.Value)
hidden = true
}
if hidden {
return ErrNotVisible
}
}
}
return nil
}))(s)
}
// ElementNotVisibleOld is a query option to wait until the element is not
// visible.
//
// This is the old, complicated, implementation (deprecated).
func ElementNotVisibleOld(s *Selector) {
WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error {
var err error
// check node has box model
_, err = dom.GetBoxModel(n.NodeID).Do(ctxt, h)
if err != nil {
return nil
}
// check if any of the parents are not visible ...
var hidden bool
for ; n.Parent != nil; n = n.Parent {
// get style
style, err := css.GetComputedStyleForNode(n.NodeID).Do(ctxt, h)
if err != nil {
return err
}
// check if hidden
for _, c := range style {
switch c.Name {
case "display":
//log.Printf("%d >>>> %s=%s", n.NodeID, c.Name, c.Value)
hidden = c.Value == "none"
case "visibility":
//log.Printf("%d >>>> %s=%s", n.NodeID, c.Name, c.Value)
hidden = c.Value != "visible"
case "hidden":
//log.Printf("%d >>>> %s=%s", n.NodeID, c.Name, c.Value)
hidden = true
}
if hidden {
return nil
}
}
}
return ErrVisible
}))(s)
}
// ElementEnabled is a query option to wait until the element is enabled.
func ElementEnabled(s *Selector) {
WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error { WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error {
n.RLock() n.RLock()
defer n.RUnlock() defer n.RUnlock()
@ -468,8 +359,8 @@ func ElementEnabled(s *Selector) {
}))(s) }))(s)
} }
// ElementSelected is a query option to wait until the element is selected. // NodeSelected is a query option to wait until the element is selected.
func ElementSelected(s *Selector) { func NodeSelected(s *Selector) {
WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error { WaitFunc(s.waitReady(func(ctxt context.Context, h cdp.Handler, n *cdp.Node) error {
n.RLock() n.RLock()
defer n.RUnlock() defer n.RUnlock()
@ -484,9 +375,9 @@ func ElementSelected(s *Selector) {
}))(s) }))(s)
} }
// ElementNotPresent is a query option to wait until no elements match are // NodeNotPresent is a query option to wait until no elements match are
// present matching the selector. // present matching the selector.
func ElementNotPresent(s *Selector) { func NodeNotPresent(s *Selector) {
s.exp = 0 s.exp = 0
WaitFunc(func(ctxt context.Context, h cdp.Handler, n *cdp.Node, ids ...cdp.NodeID) ([]*cdp.Node, error) { WaitFunc(func(ctxt context.Context, h cdp.Handler, n *cdp.Node, ids ...cdp.NodeID) ([]*cdp.Node, error) {
if len(ids) != 0 { if len(ids) != 0 {
@ -519,34 +410,26 @@ func WaitReady(sel interface{}, opts ...QueryOption) Action {
// WaitVisible waits until the selected element is visible. // WaitVisible waits until the selected element is visible.
func WaitVisible(sel interface{}, opts ...QueryOption) Action { func WaitVisible(sel interface{}, opts ...QueryOption) Action {
return Query(sel, append(opts, ElementVisible)...) return Query(sel, append(opts, NodeVisible)...)
} }
// WaitNotVisible waits until the selected element is not visible. // WaitNotVisible waits until the selected element is not visible.
func WaitNotVisible(sel interface{}, opts ...QueryOption) Action { func WaitNotVisible(sel interface{}, opts ...QueryOption) Action {
return Query(sel, append(opts, ElementNotVisible)...) return Query(sel, append(opts, NodeNotVisible)...)
} }
// WaitEnabled waits until the selected element is enabled (does not have // WaitEnabled waits until the selected element is enabled (does not have
// attribute 'disabled'). // attribute 'disabled').
func WaitEnabled(sel interface{}, opts ...QueryOption) Action { func WaitEnabled(sel interface{}, opts ...QueryOption) Action {
return Query(sel, append(opts, ElementEnabled)...) return Query(sel, append(opts, NodeEnabled)...)
} }
// WaitSelected waits until the element is selected (has attribute 'selected'). // WaitSelected waits until the element is selected (has attribute 'selected').
func WaitSelected(sel interface{}, opts ...QueryOption) Action { func WaitSelected(sel interface{}, opts ...QueryOption) Action {
return Query(sel, append(opts, ElementSelected)...) return Query(sel, append(opts, NodeSelected)...)
} }
// WaitNotPresent waits until no elements match the specified selector. // WaitNotPresent waits until no elements match the specified selector.
func WaitNotPresent(sel interface{}, opts ...QueryOption) Action { func WaitNotPresent(sel interface{}, opts ...QueryOption) Action {
return Query(sel, append(opts, ElementNotPresent)...) return Query(sel, append(opts, NodeNotPresent)...)
} }
const (
// visibleJS is a javascript snippet that returns true or false depending
// on if the specified node's offsetParent is not null.
visibleJS = `(function(a) {
return a[0].offsetParent !== null
})($x('%s'))`
)

112
util.go
View File

@ -15,11 +15,87 @@ const (
// DefaultCheckDuration is the default time to sleep between a check. // DefaultCheckDuration is the default time to sleep between a check.
DefaultCheckDuration = 50 * time.Millisecond DefaultCheckDuration = 50 * time.Millisecond
// DefaultPoolStartPort is the default start port number.
DefaultPoolStartPort = 9000
// DefaultPoolEndPort is the default end port number.
DefaultPoolEndPort = 10000
// EmptyFrameID is the "non-existent" (ie current) frame. // EmptyFrameID is the "non-existent" (ie current) frame.
EmptyFrameID cdp.FrameID = cdp.FrameID("") EmptyFrameID cdp.FrameID = cdp.FrameID("")
// EmptyNodeID is the "non-existent" node id. // EmptyNodeID is the "non-existent" node id.
EmptyNodeID cdp.NodeID = cdp.NodeID(0) EmptyNodeID cdp.NodeID = cdp.NodeID(0)
// textJS is a javascript snippet that returns the concatenated textContent
// of all visible (ie, offsetParent !== null) children.
textJS = `(function(a) {
var s = '';
for (var i = 0; i < a.length; i++) {
if (a[i].offsetParent !== null) {
s += a[i].textContent;
}
}
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.
scrollJS = `(function(x, y) {
window.scrollTo(x, y);
return [window.scrollX, window.scrollY];
})(%d, %d)`
// 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].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'))`
// valueJS is a javascript snippet that returns the value of a specified
// node.
valueJS = `(function(a) {
return a[0].value;
})($x('%s'))`
// setValueJS is a javascript snippet that sets the value of the specified
// node, and returns the value.
setValueJS = `(function(a, val) {
return a[0].value = val;
})($x('%s'), '%s')`
// visibleJS is a javascript snippet that returns true or false depending
// on if the specified node's offsetParent is not null.
visibleJS = `(function(a) {
return a[0].offsetParent !== null
})($x('%s'))`
) )
// UnmarshalMessage unmarshals the message result or params. // UnmarshalMessage unmarshals the message result or params.
@ -27,8 +103,8 @@ func UnmarshalMessage(msg *cdp.Message) (interface{}, error) {
return util.UnmarshalMessage(msg) return util.UnmarshalMessage(msg)
} }
// FrameOp is a frame manipulation operation. // frameOp is a frame manipulation operation.
type FrameOp func(*cdp.Frame) type frameOp func(*cdp.Frame)
/*func domContentEventFired(f *cdp.Frame) { /*func domContentEventFired(f *cdp.Frame) {
} }
@ -36,7 +112,7 @@ type FrameOp func(*cdp.Frame)
func loadEventFired(f *cdp.Frame) { func loadEventFired(f *cdp.Frame) {
}*/ }*/
func frameAttached(id cdp.FrameID) FrameOp { func frameAttached(id cdp.FrameID) frameOp {
return func(f *cdp.Frame) { return func(f *cdp.Frame) {
f.ParentID = id f.ParentID = id
setFrameState(f, cdp.FrameAttached) setFrameState(f, cdp.FrameAttached)
@ -82,8 +158,8 @@ func clearFrameState(f *cdp.Frame, fs cdp.FrameState) {
f.State &^= fs f.State &^= fs
} }
// NodeOp is a node manipulation operation. // nodeOp is a node manipulation operation.
type NodeOp func(*cdp.Node) type nodeOp func(*cdp.Node)
func walk(m map[cdp.NodeID]*cdp.Node, n *cdp.Node) { func walk(m map[cdp.NodeID]*cdp.Node, n *cdp.Node) {
m[n.NodeID] = n m[n.NodeID] = n
@ -117,14 +193,14 @@ func walk(m map[cdp.NodeID]*cdp.Node, n *cdp.Node) {
} }
} }
func setChildNodes(m map[cdp.NodeID]*cdp.Node, nodes []*cdp.Node) NodeOp { func setChildNodes(m map[cdp.NodeID]*cdp.Node, nodes []*cdp.Node) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.Children = nodes n.Children = nodes
walk(m, n) walk(m, n)
} }
} }
func attributeModified(name, value string) NodeOp { func attributeModified(name, value string) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
var found bool var found bool
@ -145,7 +221,7 @@ func attributeModified(name, value string) NodeOp {
} }
} }
func attributeRemoved(name string) NodeOp { func attributeRemoved(name string) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
var a []string var a []string
for i := 0; i < len(n.Attributes); i += 2 { for i := 0; i < len(n.Attributes); i += 2 {
@ -158,66 +234,66 @@ func attributeRemoved(name string) NodeOp {
} }
} }
func inlineStyleInvalidated(ids []cdp.NodeID) NodeOp { func inlineStyleInvalidated(ids []cdp.NodeID) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
} }
} }
func characterDataModified(characterData string) NodeOp { func characterDataModified(characterData string) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.Value = characterData n.Value = characterData
} }
} }
func childNodeCountUpdated(count int64) NodeOp { func childNodeCountUpdated(count int64) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.ChildNodeCount = count n.ChildNodeCount = count
} }
} }
func childNodeInserted(m map[cdp.NodeID]*cdp.Node, prevID cdp.NodeID, c *cdp.Node) NodeOp { func childNodeInserted(m map[cdp.NodeID]*cdp.Node, prevID cdp.NodeID, c *cdp.Node) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.Children = insertNode(n.Children, prevID, c) n.Children = insertNode(n.Children, prevID, c)
walk(m, n) walk(m, n)
} }
} }
func childNodeRemoved(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) NodeOp { func childNodeRemoved(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.Children = removeNode(n.Children, id) n.Children = removeNode(n.Children, id)
//delete(m, id) //delete(m, id)
} }
} }
func shadowRootPushed(m map[cdp.NodeID]*cdp.Node, c *cdp.Node) NodeOp { func shadowRootPushed(m map[cdp.NodeID]*cdp.Node, c *cdp.Node) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.ShadowRoots = append(n.ShadowRoots, c) n.ShadowRoots = append(n.ShadowRoots, c)
walk(m, n) walk(m, n)
} }
} }
func shadowRootPopped(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) NodeOp { func shadowRootPopped(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.ShadowRoots = removeNode(n.ShadowRoots, id) n.ShadowRoots = removeNode(n.ShadowRoots, id)
//delete(m, id) //delete(m, id)
} }
} }
func pseudoElementAdded(m map[cdp.NodeID]*cdp.Node, c *cdp.Node) NodeOp { func pseudoElementAdded(m map[cdp.NodeID]*cdp.Node, c *cdp.Node) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.PseudoElements = append(n.PseudoElements, c) n.PseudoElements = append(n.PseudoElements, c)
walk(m, n) walk(m, n)
} }
} }
func pseudoElementRemoved(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) NodeOp { func pseudoElementRemoved(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.PseudoElements = removeNode(n.PseudoElements, id) n.PseudoElements = removeNode(n.PseudoElements, id)
//delete(m, id) //delete(m, id)
} }
} }
func distributedNodesUpdated(nodes []*cdp.BackendNode) NodeOp { func distributedNodesUpdated(nodes []*cdp.BackendNode) nodeOp {
return func(n *cdp.Node) { return func(n *cdp.Node) {
n.DistributedNodes = nodes n.DistributedNodes = nodes
} }