chromedp/query.go
Kenneth Shaw b57afce7e7 Fixing Evaluate and EvaluateAsDevTools actions
- Update Evaluate action to also work with []byte
- Update Evaluate documentation
- Fix EvaluateAsDevTools action
- Refactor Query actions to use EvaluateAsDevTools
2017-02-08 15:18:55 +07:00

453 lines
12 KiB
Go

package chromedp
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/png"
"strings"
"sync"
"github.com/disintegration/imaging"
"github.com/knq/chromedp/cdp"
"github.com/knq/chromedp/cdp/dom"
"github.com/knq/chromedp/cdp/input"
"github.com/knq/chromedp/cdp/page"
)
var (
// ErrInvalidBoxModel is the error returned when the retrieved box model is
// invalid.
ErrInvalidBoxModel = errors.New("invalid box model")
)
// 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 {
if len(nodes) < 1 {
return fmt.Errorf("selector `%s` did not return any nodes", sel)
}
return dom.Focus(nodes[0].NodeID).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 {
if len(nodes) < 1 {
return fmt.Errorf("selector `%s` did not return any nodes", sel)
}
for _, n := range nodes {
if n.NodeType != cdp.NodeTypeElement || (n.NodeName != "INPUT" && n.NodeName != "TEXTAREA") {
return fmt.Errorf("selector `%s` matched node %d with name %s", sel, n.NodeID, strings.ToLower(n.NodeName))
}
}
errs := make([]error, len(nodes))
wg := new(sync.WaitGroup)
for i, n := range nodes {
wg.Add(1)
go func(i int, n *cdp.Node) {
defer wg.Done()
var a Action
if n.NodeName == "INPUT" {
a = dom.SetAttributeValue(n.NodeID, "value", "")
} else {
a = dom.SetNodeValue(n.NodeID, "")
}
errs[i] = a.Do(ctxt, h)
}(i, n)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return err
}
}
return nil
}, 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 {
if model == nil {
panic("model 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)
}
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 {
if value == nil {
panic("value 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(valueJS, nodes[0].FullXPath()), value).Do(ctxt, h)
}, opts...)
}
// SetValue sets the value of an element.
func SetValue(sel interface{}, value string, 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 string
err := EvaluateAsDevTools(fmt.Sprintf(setValueJS, nodes[0].FullXPath(), value), &res).Do(ctxt, h)
if err != nil {
return err
}
if res != value {
return fmt.Errorf("could not set value on 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...)
}
// Attributes retrieves the attributes for the specified element.
func Attributes(sel interface{}, attributes *map[string]string, opts ...QueryOption) Action {
if attributes == nil {
panic("attributes 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)
}
nodes[0].RLock()
defer nodes[0].RUnlock()
m := make(map[string]string)
attrs := nodes[0].Attributes
for i := 0; i < len(attrs); i += 2 {
m[attrs[i]] = attrs[i+1]
}
*attributes = m
return nil
}, opts...)
}
// SetAttributes sets the attributes for the specified element.
func SetAttributes(sel interface{}, attributes map[string]string, opts ...QueryOption) Action {
return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error {
if len(nodes) < 1 {
return errors.New("expected at least one element")
}
return nil
}, opts...)
}
// AttributeValue retrieves the name'd attribute value for the specified
// element.
func AttributeValue(sel interface{}, name string, value *string, ok *bool, opts ...QueryOption) Action {
if value == nil {
panic("value cannot be nil")
}
return QueryAfter(sel, func(ctxt context.Context, h cdp.FrameHandler, nodes ...*cdp.Node) error {
if len(nodes) < 1 {
return errors.New("expected at least one element")
}
nodes[0].RLock()
defer nodes[0].RUnlock()
attrs := nodes[0].Attributes
for i := 0; i < len(attrs); i += 2 {
if attrs[i] == name {
*value = attrs[i+1]
if ok != nil {
*ok = true
}
return nil
}
}
if ok != nil {
*ok = false
}
return nil
}, opts...)
}
// SetAttributeValue sets an element's attribute with name to value.
func SetAttributeValue(sel interface{}, name, value string, 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 dom.SetAttributeValue(nodes[0].NodeID, name, value).Do(ctxt, h)
}, opts...)
}
// RemoveAttribute removes an element's attribute with name.
func RemoveAttribute(sel interface{}, name string, 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 dom.RemoveAttribute(nodes[0].NodeID, name).Do(ctxt, h)
}, opts...)
}
// Click sends a click to the first element returned by the selector.
func Click(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], Button(input.ButtonLeft), ClickCount(1)).Do(ctxt, h)
}, append(opts, ElementVisible)...)
}
// DoubleClick does a double click on the first element returned by selector.
func DoubleClick(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], Button(input.ButtonLeft), ClickCount(2)).Do(ctxt, h)
}, append(opts, ElementVisible)...)
}
// 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], Button(input.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 {
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 KeyActionNode(nodes[0], v).Do(ctxt, h)
}, append(opts, ElementVisible)...)
}
// Screenshot takes a screenshot of the first element matching the selector.
func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
if picbuf == nil {
panic("picbuf 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)
}
var err error
// get box model
box, err := dom.GetBoxModel(nodes[0].NodeID).Do(ctxt, h)
if err != nil {
return err
}
// check box
if len(box.Margin) != 8 {
return ErrInvalidBoxModel
}
// scroll to node location
var pos []int
err = EvaluateAsDevTools(fmt.Sprintf(scrollJS, int64(box.Margin[0]), int64(box.Margin[1])), &pos).Do(ctxt, h)
if err != nil {
return err
}
// take page screenshot
buf, err := page.CaptureScreenshot().Do(ctxt, h)
if err != nil {
return err
}
// load image
img, err := png.Decode(bytes.NewReader(buf))
if err != nil {
return err
}
// crop to box model contents.
cropped := imaging.Crop(img, image.Rect(
int(box.Margin[0])-pos[0], int(box.Margin[1])-pos[1],
int(box.Margin[4])-pos[0], int(box.Margin[5])-pos[1],
))
// encode
var croppedBuf bytes.Buffer
err = png.Encode(&croppedBuf, cropped)
if err != nil {
return err
}
*picbuf = croppedBuf.Bytes()
return nil
}, append(opts, ElementVisible)...)
}
// Submit is an action that submits whatever form the first element 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 {
return fmt.Errorf("selector `%s` did not return any nodes", sel)
}
var res bool
err := EvaluateAsDevTools(fmt.Sprintf(submitJS, nodes[0].FullXPath()), &res).Do(ctxt, h)
if err != nil {
return err
}
if !res {
return fmt.Errorf("submit on node %d returned false", nodes[0].NodeID)
}
return nil
}, opts...)
}
const (
// 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()"))`
// 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].form !== null) {
return a[0].form.submit();
}
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')`
)
/*
ScrollTo
Title
SetTitle
OuterHTML
SetOuterHTML
NodeName -- ?
Style(Matched)
Style(Computed)
SetStyle
GetStyle(Inline)
*/