Compare commits
62 Commits
Author | SHA1 | Date | |
---|---|---|---|
ad82438599 | |||
a0bba77505 | |||
|
f606ca9e73 | ||
|
1a54253acd | ||
|
958088f83b | ||
|
e4c16681d0 | ||
|
71ae9f7bbc | ||
|
ac47d6ba0e | ||
|
92a77355f6 | ||
|
46982a1cac | ||
|
e8122e4a26 | ||
|
b481eeac51 | ||
|
b8efcf0691 | ||
|
a29b1ec1d6 | ||
|
11b3a5dc8f | ||
|
d0484ed1c5 | ||
|
687cf6d766 | ||
|
939d377090 | ||
|
b977e305d2 | ||
|
c41ed01b6a | ||
|
c313fa1c1d | ||
|
b647c708b4 | ||
|
97e80a00d5 | ||
|
504561eab2 | ||
|
65a198c84e | ||
|
ece2b3ab92 | ||
|
896fbe60c2 | ||
|
e482cdfc4d | ||
|
120628a01c | ||
|
7c8529b914 | ||
|
41e913e571 | ||
|
ad8809efb7 | ||
|
0d568ec2a4 | ||
|
fb23c1750a | ||
|
d73caffcd0 | ||
|
1decbccd74 | ||
|
117274bc5d | ||
|
661ef78880 | ||
|
8ff2971fc5 | ||
|
a0a36956a8 | ||
|
2b925df0fb | ||
|
f742f327a7 | ||
|
0e92de5e65 | ||
|
32d4bae280 | ||
|
a93c63124f | ||
|
b136a6267e | ||
|
e698c943b3 | ||
|
5fb1c07412 | ||
|
2ca3ea3591 | ||
|
6fb5264bbd | ||
|
da4ac414ed | ||
|
7c1a9fbf3e | ||
|
c109f6ebfd | ||
|
61f0a8da68 | ||
|
92bfcc3c8d | ||
|
24decf54d3 | ||
|
81a48280ef | ||
|
3d3bf22ccc | ||
|
5aca12cc3e | ||
|
e9aa66f87e | ||
|
39bd95c850 | ||
|
b61de69d62 |
2
.github/ISSUE_TEMPLATE
vendored
2
.github/ISSUE_TEMPLATE
vendored
@ -1,7 +1,7 @@
|
|||||||
#### What versions are you running?
|
#### What versions are you running?
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
$ go list -m github.com/chromedp/chromedp
|
$ go list -m git.loafle.net/commons_go/chromedp
|
||||||
$ chromium --version
|
$ chromium --version
|
||||||
$ go version
|
$ go version
|
||||||
</pre>
|
</pre>
|
||||||
|
13
.travis.yml
13
.travis.yml
@ -1,14 +1,11 @@
|
|||||||
language: go
|
language: go
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.10.x
|
- 1.12.x
|
||||||
- 1.11.x
|
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
chrome: stable
|
chrome: stable
|
||||||
before_install:
|
|
||||||
- go get github.com/mattn/goveralls golang.org/x/vgo
|
|
||||||
script:
|
script:
|
||||||
- export CHROMEDP_TEST_RUNNER=google-chrome-stable
|
- go test -v ./...
|
||||||
- export CHROMEDP_DISABLE_GPU=true
|
|
||||||
- vgo test -v -coverprofile=coverage.out
|
|
||||||
- goveralls -service=travis-ci -coverprofile=coverage.out
|
|
||||||
|
12
README.md
12
README.md
@ -9,13 +9,14 @@ Package chromedp is a faster, simpler way to drive browsers supporting the
|
|||||||
Install in the usual Go way:
|
Install in the usual Go way:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get -u github.com/chromedp/chromedp
|
go get -u git.loafle.net/commons_go/chromedp
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Please see the [examples][6] project for more examples. Please refer to the
|
Please see the [examples][6] project for more examples. Please refer to the
|
||||||
[GoDoc API listing][7] for a summary of the API and Actions.
|
[GoDoc API listing][7] for a summary of the API and Actions, which also contains
|
||||||
|
a few simple and runnable examples.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
@ -24,11 +25,10 @@ Please see the [examples][6] project for more examples. Please refer to the
|
|||||||
* [chromedp examples][6] - various `chromedp` examples
|
* [chromedp examples][6] - various `chromedp` examples
|
||||||
* [`github.com/chromedp/cdproto`][9] - GoDoc listing for the CDP domains used by `chromedp`
|
* [`github.com/chromedp/cdproto`][9] - GoDoc listing for the CDP domains used by `chromedp`
|
||||||
* [`github.com/chromedp/cdproto-gen`][10] - tool used to generate `cdproto`
|
* [`github.com/chromedp/cdproto-gen`][10] - tool used to generate `cdproto`
|
||||||
* [`github.com/chromedp/chromedp-proxy`][11] - a simple CDP proxy for logging CDP clients and browsers
|
* [`git.loafle.net/commons_go/chromedp-proxy`][11] - a simple CDP proxy for logging CDP clients and browsers
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
* Move timeouts to context (defaults)
|
|
||||||
* Implement more query selector options (allow over riding context timeouts)
|
* Implement more query selector options (allow over riding context timeouts)
|
||||||
* Contextual actions for "dry run" (or via an accumulator?)
|
* Contextual actions for "dry run" (or via an accumulator?)
|
||||||
* Network loader / manager
|
* Network loader / manager
|
||||||
@ -40,8 +40,8 @@ Please see the [examples][6] project for more examples. Please refer to the
|
|||||||
[4]: https://coveralls.io/github/chromedp/chromedp?branch=master
|
[4]: https://coveralls.io/github/chromedp/chromedp?branch=master
|
||||||
[5]: https://chromedevtools.github.io/devtools-protocol/
|
[5]: https://chromedevtools.github.io/devtools-protocol/
|
||||||
[6]: https://github.com/chromedp/examples
|
[6]: https://github.com/chromedp/examples
|
||||||
[7]: https://godoc.org/github.com/chromedp/chromedp
|
[7]: https://godoc.org/git.loafle.net/commons_go/chromedp
|
||||||
[8]: https://www.youtube.com/watch?v=_7pWCg94sKw
|
[8]: https://www.youtube.com/watch?v=_7pWCg94sKw
|
||||||
[9]: https://godoc.org/github.com/chromedp/cdproto
|
[9]: https://godoc.org/github.com/chromedp/cdproto
|
||||||
[10]: https://github.com/chromedp/cdproto-gen
|
[10]: https://github.com/chromedp/cdproto-gen
|
||||||
[11]: https://github.com/chromedp/chromedp-proxy
|
[11]: https://git.loafle.net/commons_go/chromedp-proxy
|
||||||
|
23
actions.go
23
actions.go
@ -18,8 +18,8 @@ type Action interface {
|
|||||||
type ActionFunc func(context.Context, cdp.Executor) error
|
type ActionFunc func(context.Context, cdp.Executor) error
|
||||||
|
|
||||||
// Do executes the func f using the provided context and frame handler.
|
// Do executes the func f using the provided context and frame handler.
|
||||||
func (f ActionFunc) Do(ctxt context.Context, h cdp.Executor) error {
|
func (f ActionFunc) Do(ctx context.Context, h cdp.Executor) error {
|
||||||
return f(ctxt, h)
|
return f(ctx, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tasks is a sequential list of Actions that can be used as a single Action.
|
// Tasks is a sequential list of Actions that can be used as a single Action.
|
||||||
@ -27,12 +27,12 @@ type Tasks []Action
|
|||||||
|
|
||||||
// Do executes the list of Actions sequentially, using the provided context and
|
// Do executes the list of Actions sequentially, using the provided context and
|
||||||
// frame handler.
|
// frame handler.
|
||||||
func (t Tasks) Do(ctxt context.Context, h cdp.Executor) error {
|
func (t Tasks) Do(ctx context.Context, h cdp.Executor) error {
|
||||||
// TODO: put individual task timeouts from context here
|
// TODO: put individual task timeouts from context here
|
||||||
for _, a := range t {
|
for _, a := range t {
|
||||||
// ctxt, cancel = context.WithTimeout(ctxt, timeout)
|
// ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||||
// defer cancel()
|
// defer cancel()
|
||||||
if err := a.Do(ctxt, h); err != nil {
|
if err := a.Do(ctx, h); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,12 +46,15 @@ func (t Tasks) Do(ctxt context.Context, h cdp.Executor) error {
|
|||||||
// be marked for deprecation in the future, after the remaining Actions have
|
// be marked for deprecation in the future, after the remaining Actions have
|
||||||
// been able to be written/tested.
|
// been able to be written/tested.
|
||||||
func Sleep(d time.Duration) Action {
|
func Sleep(d time.Duration) Action {
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
|
// Don't use time.After, to avoid a temporary goroutine leak if
|
||||||
|
// ctx is cancelled before the timer fires.
|
||||||
|
t := time.NewTimer(d)
|
||||||
select {
|
select {
|
||||||
case <-time.After(d):
|
case <-t.C:
|
||||||
|
case <-ctx.Done():
|
||||||
case <-ctxt.Done():
|
t.Stop()
|
||||||
return ctxt.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
288
allocate.go
Normal file
288
allocate.go
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
package chromedp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An Allocator is responsible for creating and managing a number of browsers.
|
||||||
|
//
|
||||||
|
// This interface abstracts away how the browser process is actually run. For
|
||||||
|
// example, an Allocator implementation may reuse browser processes, or connect
|
||||||
|
// to already-running browsers on remote machines.
|
||||||
|
type Allocator interface {
|
||||||
|
// Allocate creates a new browser. It can be cancelled via the provided
|
||||||
|
// context, at which point all the resources used by the browser (such
|
||||||
|
// as temporary directories) will be freed.
|
||||||
|
Allocate(context.Context, ...BrowserOption) (*Browser, error)
|
||||||
|
|
||||||
|
// Wait blocks until an allocator has freed all of its resources.
|
||||||
|
// Cancelling the allocator context will already perform this operation,
|
||||||
|
// so normally there's no need to call Wait directly.
|
||||||
|
Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupExecAllocator is similar to NewExecAllocator, but it allows NewContext
|
||||||
|
// to create the allocator without the unnecessary context layer.
|
||||||
|
func setupExecAllocator(opts ...ExecAllocatorOption) *ExecAllocator {
|
||||||
|
ep := &ExecAllocator{
|
||||||
|
initFlags: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(ep)
|
||||||
|
}
|
||||||
|
if ep.execPath == "" {
|
||||||
|
ep.execPath = findExecPath()
|
||||||
|
}
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultExecAllocatorOptions are the ExecAllocator options used by NewContext
|
||||||
|
// if the given parent context doesn't have an allocator set up.
|
||||||
|
var DefaultExecAllocatorOptions = []ExecAllocatorOption{
|
||||||
|
NoFirstRun,
|
||||||
|
NoDefaultBrowserCheck,
|
||||||
|
Headless,
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExecAllocator creates a new context set up with an ExecAllocator, suitable
|
||||||
|
// for use with NewContext.
|
||||||
|
func NewExecAllocator(parent context.Context, opts ...ExecAllocatorOption) (context.Context, context.CancelFunc) {
|
||||||
|
ctx, cancel := context.WithCancel(parent)
|
||||||
|
c := &Context{Allocator: setupExecAllocator(opts...)}
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, contextKey{}, c)
|
||||||
|
cancelWait := func() {
|
||||||
|
cancel()
|
||||||
|
c.Allocator.Wait()
|
||||||
|
}
|
||||||
|
return ctx, cancelWait
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecAllocatorOption is a exec allocator option.
|
||||||
|
type ExecAllocatorOption func(*ExecAllocator)
|
||||||
|
|
||||||
|
// ExecAllocator is an Allocator which starts new browser processes on the host
|
||||||
|
// machine.
|
||||||
|
type ExecAllocator struct {
|
||||||
|
execPath string
|
||||||
|
initFlags map[string]interface{}
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate satisfies the Allocator interface.
|
||||||
|
func (p *ExecAllocator) Allocate(ctx context.Context, opts ...BrowserOption) (*Browser, error) {
|
||||||
|
c := FromContext(ctx)
|
||||||
|
if c == nil {
|
||||||
|
return nil, ErrInvalidContext
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
for name, value := range p.initFlags {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
args = append(args, fmt.Sprintf("--%s=%s", name, value))
|
||||||
|
case bool:
|
||||||
|
if value {
|
||||||
|
args = append(args, fmt.Sprintf("--%s", name))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid exec pool flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDir := false
|
||||||
|
dataDir, ok := p.initFlags["user-data-dir"].(string)
|
||||||
|
if !ok {
|
||||||
|
tempDir, err := ioutil.TempDir("", "chromedp-runner")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args = append(args, "--user-data-dir="+tempDir)
|
||||||
|
dataDir = tempDir
|
||||||
|
removeDir = true
|
||||||
|
}
|
||||||
|
args = append(args, "--remote-debugging-port=0")
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
p.wg.Add(1) // for the entire allocator
|
||||||
|
c.wg.Add(1) // for this browser's root context
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
// First wait for the process to be finished.
|
||||||
|
if cmd != nil {
|
||||||
|
// TODO: do we care about this error in any scenario? if
|
||||||
|
// the user cancelled the context and killed chrome,
|
||||||
|
// this will most likely just be "signal: killed", which
|
||||||
|
// isn't interesting.
|
||||||
|
cmd.Wait()
|
||||||
|
}
|
||||||
|
// Then delete the temporary user data directory, if needed.
|
||||||
|
if removeDir {
|
||||||
|
if err := os.RemoveAll(dataDir); c.cancelErr == nil {
|
||||||
|
c.cancelErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.wg.Done()
|
||||||
|
c.wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// force the first page to be blank, instead of the welcome page
|
||||||
|
// TODO: why isn't --no-first-run enough?
|
||||||
|
args = append(args, "about:blank")
|
||||||
|
|
||||||
|
cmd = exec.CommandContext(ctx, p.execPath, args...)
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick up the browser's websocket URL from stderr.
|
||||||
|
wsURL := ""
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
prefix := "DevTools listening on"
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if s := strings.TrimPrefix(line, prefix); s != line {
|
||||||
|
wsURL = strings.TrimSpace(s)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stderr.Close()
|
||||||
|
|
||||||
|
browser, err := NewBrowser(ctx, wsURL, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
browser.process = cmd.Process
|
||||||
|
browser.userDataDir = dataDir
|
||||||
|
return browser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait satisfies the Allocator interface.
|
||||||
|
func (p *ExecAllocator) Wait() {
|
||||||
|
p.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecPath returns an ExecAllocatorOption which uses the given path to execute
|
||||||
|
// browser processes. The given path can be an absolute path to a binary, or
|
||||||
|
// just the name of the program to find via exec.LookPath.
|
||||||
|
func ExecPath(path string) ExecAllocatorOption {
|
||||||
|
return func(p *ExecAllocator) {
|
||||||
|
if fullPath, _ := exec.LookPath(path); fullPath != "" {
|
||||||
|
// Convert to an absolute path if possible, to avoid
|
||||||
|
// repeated LookPath calls in each Allocate.
|
||||||
|
path = fullPath
|
||||||
|
}
|
||||||
|
p.execPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// findExecPath tries to find the Chrome browser somewhere in the current
|
||||||
|
// system. It performs a rather agressive search, which is the same in all
|
||||||
|
// systems. That may make it a bit slow, but it will only be run when creating a
|
||||||
|
// new ExecAllocator.
|
||||||
|
func findExecPath() string {
|
||||||
|
for _, path := range [...]string{
|
||||||
|
// Unix-like
|
||||||
|
"headless_shell",
|
||||||
|
"headless-shell",
|
||||||
|
"chromium",
|
||||||
|
"chromium-browser",
|
||||||
|
"google-chrome",
|
||||||
|
"google-chrome-stable",
|
||||||
|
"google-chrome-beta",
|
||||||
|
"google-chrome-unstable",
|
||||||
|
"/usr/bin/google-chrome",
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
"chrome",
|
||||||
|
"chrome.exe", // in case PATHEXT is misconfigured
|
||||||
|
`C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`,
|
||||||
|
|
||||||
|
// Mac
|
||||||
|
`/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
||||||
|
} {
|
||||||
|
found, err := exec.LookPath(path)
|
||||||
|
if err == nil {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to something simple and sensible, to give a useful error
|
||||||
|
// message.
|
||||||
|
return "google-chrome"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag is a generic command line option to pass a flag to Chrome. If the value
|
||||||
|
// is a string, it will be passed as --name=value. If it's a boolean, it will be
|
||||||
|
// passed as --name if value is true.
|
||||||
|
func Flag(name string, value interface{}) ExecAllocatorOption {
|
||||||
|
return func(p *ExecAllocator) {
|
||||||
|
p.initFlags[name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDataDir is the command line option to set the user data dir.
|
||||||
|
//
|
||||||
|
// Note: set this option to manually set the profile directory used by Chrome.
|
||||||
|
// When this is not set, then a default path will be created in the /tmp
|
||||||
|
// directory.
|
||||||
|
func UserDataDir(dir string) ExecAllocatorOption {
|
||||||
|
return Flag("user-data-dir", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyServer is the command line option to set the outbound proxy server.
|
||||||
|
func ProxyServer(proxy string) ExecAllocatorOption {
|
||||||
|
return Flag("proxy-server", proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowSize is the command line option to set the initial window size.
|
||||||
|
func WindowSize(width, height int) ExecAllocatorOption {
|
||||||
|
return Flag("window-size", fmt.Sprintf("%d,%d", width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgent is the command line option to set the default User-Agent
|
||||||
|
// header.
|
||||||
|
func UserAgent(userAgent string) ExecAllocatorOption {
|
||||||
|
return Flag("user-agent", userAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoSandbox is the Chrome comamnd line option to disable the sandbox.
|
||||||
|
func NoSandbox(p *ExecAllocator) {
|
||||||
|
Flag("no-sandbox", true)(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoFirstRun is the Chrome comamnd line option to disable the first run
|
||||||
|
// dialog.
|
||||||
|
func NoFirstRun(p *ExecAllocator) {
|
||||||
|
Flag("no-first-run", true)(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoDefaultBrowserCheck is the Chrome comamnd line option to disable the
|
||||||
|
// default browser check.
|
||||||
|
func NoDefaultBrowserCheck(p *ExecAllocator) {
|
||||||
|
Flag("no-default-browser-check", true)(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headless is the command line option to run in headless mode.
|
||||||
|
func Headless(p *ExecAllocator) {
|
||||||
|
Flag("headless", true)(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableGPU is the command line option to disable the GPU process.
|
||||||
|
func DisableGPU(p *ExecAllocator) {
|
||||||
|
Flag("disable-gpu", true)(p)
|
||||||
|
}
|
76
allocate_test.go
Normal file
76
allocate_test.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package chromedp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExecAllocator(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
allocCtx, cancel := NewExecAllocator(context.Background(), allocOpts...)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// TODO: test that multiple child contexts are run in different
|
||||||
|
// processes and browsers.
|
||||||
|
|
||||||
|
taskCtx, cancel := NewContext(allocCtx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
want := "insert"
|
||||||
|
var got string
|
||||||
|
if err := Run(taskCtx,
|
||||||
|
Navigate(testdataDir+"/form.html"),
|
||||||
|
Text("#foo", &got, ByID),
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("wanted %q, got %q", want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
tempDir := FromContext(taskCtx).Browser.userDataDir
|
||||||
|
if _, err := os.Lstat(tempDir); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("temporary user data dir %q not deleted", tempDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecAllocatorCancelParent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
allocCtx, allocCancel := NewExecAllocator(context.Background(), allocOpts...)
|
||||||
|
defer allocCancel()
|
||||||
|
|
||||||
|
// TODO: test that multiple child contexts are run in different
|
||||||
|
// processes and browsers.
|
||||||
|
|
||||||
|
taskCtx, _ := NewContext(allocCtx)
|
||||||
|
if err := Run(taskCtx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canceling the pool context should stop all browsers too.
|
||||||
|
allocCancel()
|
||||||
|
|
||||||
|
tempDir := FromContext(taskCtx).Browser.userDataDir
|
||||||
|
if _, err := os.Lstat(tempDir); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("temporary user data dir %q not deleted", tempDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSkipNewContext(t *testing.T) {
|
||||||
|
ctx, cancel := NewExecAllocator(context.Background(), allocOpts...)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Using the allocator context directly (without calling NewContext)
|
||||||
|
// should be an immediate error.
|
||||||
|
err := Run(ctx, Navigate(testdataDir+"/form.html"))
|
||||||
|
|
||||||
|
want := ErrInvalidContext
|
||||||
|
if err != want {
|
||||||
|
t.Fatalf("want error to be %q, got %q", want, err)
|
||||||
|
}
|
||||||
|
}
|
321
browser.go
Normal file
321
browser.go
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
package chromedp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto"
|
||||||
|
"github.com/chromedp/cdproto/cdp"
|
||||||
|
"github.com/chromedp/cdproto/runtime"
|
||||||
|
"github.com/chromedp/cdproto/target"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Browser is the high-level Chrome DevTools Protocol browser manager, handling
|
||||||
|
// the browser process runner, WebSocket clients, associated targets, and
|
||||||
|
// network, page, and DOM events.
|
||||||
|
type Browser struct {
|
||||||
|
conn Transport
|
||||||
|
|
||||||
|
// next is the next message id.
|
||||||
|
next int64
|
||||||
|
|
||||||
|
// tabQueue is the queue used to create new target handlers, once a new
|
||||||
|
// tab is created and attached to. The newly created Target is sent back
|
||||||
|
// via tabResult.
|
||||||
|
tabQueue chan newTab
|
||||||
|
tabResult chan *Target
|
||||||
|
|
||||||
|
// cmdQueue is the outgoing command queue.
|
||||||
|
cmdQueue chan cmdJob
|
||||||
|
|
||||||
|
// logging funcs
|
||||||
|
logf func(string, ...interface{})
|
||||||
|
errf func(string, ...interface{})
|
||||||
|
dbgf func(string, ...interface{})
|
||||||
|
|
||||||
|
// The optional fields below are helpful for some tests.
|
||||||
|
|
||||||
|
// process can be initialized by the allocators which start a process
|
||||||
|
// when allocating a browser.
|
||||||
|
process *os.Process
|
||||||
|
|
||||||
|
// userDataDir can be initialized by the allocators which set up user
|
||||||
|
// data dirs directly.
|
||||||
|
userDataDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
type newTab struct {
|
||||||
|
targetID target.ID
|
||||||
|
sessionID target.SessionID
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmdJob struct {
|
||||||
|
msg *cdproto.Message
|
||||||
|
resp chan *cdproto.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBrowser creates a new browser.
|
||||||
|
func NewBrowser(ctx context.Context, urlstr string, opts ...BrowserOption) (*Browser, error) {
|
||||||
|
b := &Browser{
|
||||||
|
tabQueue: make(chan newTab, 1),
|
||||||
|
tabResult: make(chan *Target, 1),
|
||||||
|
cmdQueue: make(chan cmdJob),
|
||||||
|
logf: log.Printf,
|
||||||
|
}
|
||||||
|
// apply options
|
||||||
|
for _, o := range opts {
|
||||||
|
o(b)
|
||||||
|
}
|
||||||
|
// ensure errf is set
|
||||||
|
if b.errf == nil {
|
||||||
|
b.errf = func(s string, v ...interface{}) { b.logf("ERROR: "+s, v...) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// dial
|
||||||
|
var err error
|
||||||
|
b.conn, err = DialContext(ctx, ForceIP(urlstr), WithConnDebugf(b.dbgf))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go b.run(ctx)
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Browser) newExecutorForTarget(ctx context.Context, targetID target.ID, sessionID target.SessionID) *Target {
|
||||||
|
if targetID == "" {
|
||||||
|
panic("empty target ID")
|
||||||
|
}
|
||||||
|
if sessionID == "" {
|
||||||
|
panic("empty session ID")
|
||||||
|
}
|
||||||
|
b.tabQueue <- newTab{targetID, sessionID}
|
||||||
|
return <-b.tabResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Browser) Execute(ctx context.Context, method string, params json.Marshaler, res json.Unmarshaler) error {
|
||||||
|
paramsMsg := emptyObj
|
||||||
|
if params != nil {
|
||||||
|
var err error
|
||||||
|
if paramsMsg, err = json.Marshal(params); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id := atomic.AddInt64(&b.next, 1)
|
||||||
|
ch := make(chan *cdproto.Message, 1)
|
||||||
|
b.cmdQueue <- cmdJob{
|
||||||
|
msg: &cdproto.Message{
|
||||||
|
ID: id,
|
||||||
|
Method: cdproto.MethodType(method),
|
||||||
|
Params: paramsMsg,
|
||||||
|
},
|
||||||
|
resp: ch,
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case msg := <-ch:
|
||||||
|
switch {
|
||||||
|
case msg == nil:
|
||||||
|
return ErrChannelClosed
|
||||||
|
case msg.Error != nil:
|
||||||
|
return msg.Error
|
||||||
|
case res != nil:
|
||||||
|
return json.Unmarshal(msg.Result, res)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tabEvent struct {
|
||||||
|
sessionID target.SessionID
|
||||||
|
msg *cdproto.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Browser) run(ctx context.Context) {
|
||||||
|
defer b.conn.Close()
|
||||||
|
|
||||||
|
cancel := FromContext(ctx).cancel
|
||||||
|
|
||||||
|
// tabEventQueue is the queue of incoming target events, to be routed by
|
||||||
|
// their session ID.
|
||||||
|
tabEventQueue := make(chan tabEvent, 1)
|
||||||
|
|
||||||
|
// resQueue is the incoming command result queue.
|
||||||
|
resQueue := make(chan *cdproto.Message, 1)
|
||||||
|
|
||||||
|
// This goroutine continuously reads events from the websocket
|
||||||
|
// connection. The separate goroutine is needed since a websocket read
|
||||||
|
// is blocking, so it cannot be used in a select statement.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
msg, err := b.conn.Read()
|
||||||
|
if err != nil {
|
||||||
|
// If the websocket failed, most likely Chrome
|
||||||
|
// was closed or crashed. Cancel the entire
|
||||||
|
// Browser context to stop all activity.
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if msg.Method == cdproto.EventRuntimeExceptionThrown {
|
||||||
|
ev := new(runtime.EventExceptionThrown)
|
||||||
|
if err := json.Unmarshal(msg.Params, ev); err != nil {
|
||||||
|
b.errf("%s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.errf("%+v\n", ev.ExceptionDetails)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionID target.SessionID
|
||||||
|
if msg.Method == cdproto.EventTargetReceivedMessageFromTarget {
|
||||||
|
event := new(target.EventReceivedMessageFromTarget)
|
||||||
|
if err := json.Unmarshal(msg.Params, event); err != nil {
|
||||||
|
b.errf("%s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sessionID = event.SessionID
|
||||||
|
msg = new(cdproto.Message)
|
||||||
|
if err := json.Unmarshal([]byte(event.Message), msg); err != nil {
|
||||||
|
b.errf("%s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case msg.Method != "":
|
||||||
|
if sessionID == "" {
|
||||||
|
// TODO: are we interested in browser events?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tabEventQueue <- tabEvent{
|
||||||
|
sessionID: sessionID,
|
||||||
|
msg: msg,
|
||||||
|
}
|
||||||
|
case msg.ID != 0:
|
||||||
|
// We can't process the response here, as it's
|
||||||
|
// another goroutine that maintans respByID.
|
||||||
|
resQueue <- msg
|
||||||
|
default:
|
||||||
|
b.errf("ignoring malformed incoming message (missing id or method): %#v", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// This goroutine handles tabs, as well as routing events to each tab
|
||||||
|
// via the pages map.
|
||||||
|
go func() {
|
||||||
|
// This map is only safe for use within this goroutine, so don't
|
||||||
|
// declare it as a Browser field.
|
||||||
|
pages := make(map[target.SessionID]*Target, 1024)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case tab := <-b.tabQueue:
|
||||||
|
if _, ok := pages[tab.sessionID]; ok {
|
||||||
|
b.errf("executor for %q already exists", tab.sessionID)
|
||||||
|
}
|
||||||
|
t := &Target{
|
||||||
|
browser: b,
|
||||||
|
TargetID: tab.targetID,
|
||||||
|
SessionID: tab.sessionID,
|
||||||
|
|
||||||
|
eventQueue: make(chan *cdproto.Message, 1024),
|
||||||
|
waitQueue: make(chan func(cur *cdp.Frame) bool, 1024),
|
||||||
|
frames: make(map[cdp.FrameID]*cdp.Frame),
|
||||||
|
|
||||||
|
logf: b.logf,
|
||||||
|
errf: b.errf,
|
||||||
|
}
|
||||||
|
go t.run(ctx)
|
||||||
|
pages[tab.sessionID] = t
|
||||||
|
b.tabResult <- t
|
||||||
|
case event := <-tabEventQueue:
|
||||||
|
page, ok := pages[event.sessionID]
|
||||||
|
if !ok {
|
||||||
|
b.errf("unknown session ID %q", event.sessionID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case page.eventQueue <- event.msg:
|
||||||
|
default:
|
||||||
|
panic("eventQueue is full")
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
respByID := make(map[int64]chan *cdproto.Message)
|
||||||
|
|
||||||
|
// This goroutine handles sending commands to the browser, and sending
|
||||||
|
// responses back for each of these commands via respByID.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case res := <-resQueue:
|
||||||
|
resp, ok := respByID[res.ID]
|
||||||
|
if !ok {
|
||||||
|
b.errf("id %d not present in response map", res.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
// resp could be nil, if we're not interested in
|
||||||
|
// this response; for CommandSendMessageToTarget.
|
||||||
|
resp <- res
|
||||||
|
close(resp)
|
||||||
|
}
|
||||||
|
delete(respByID, res.ID)
|
||||||
|
|
||||||
|
case q := <-b.cmdQueue:
|
||||||
|
if _, ok := respByID[q.msg.ID]; ok {
|
||||||
|
b.errf("id %d already present in response map", q.msg.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
respByID[q.msg.ID] = q.resp
|
||||||
|
|
||||||
|
if q.msg.Method == "" {
|
||||||
|
// Only register the chananel in respByID;
|
||||||
|
// useful for CommandSendMessageToTarget.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := b.conn.Write(q.msg); err != nil {
|
||||||
|
b.errf("%s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BrowserOption is a browser option.
|
||||||
|
type BrowserOption func(*Browser)
|
||||||
|
|
||||||
|
// WithBrowserLogf is a browser option to specify a func to receive general logging.
|
||||||
|
func WithBrowserLogf(f func(string, ...interface{})) BrowserOption {
|
||||||
|
return func(b *Browser) { b.logf = f }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBrowserErrorf is a browser option to specify a func to receive error logging.
|
||||||
|
func WithBrowserErrorf(f func(string, ...interface{})) BrowserOption {
|
||||||
|
return func(b *Browser) { b.errf = f }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBrowserDebugf is a browser option to specify a func to log actual
|
||||||
|
// websocket messages.
|
||||||
|
func WithBrowserDebugf(f func(string, ...interface{})) BrowserOption {
|
||||||
|
return func(b *Browser) { b.dbgf = f }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithConsolef is a browser option to specify a func to receive chrome log events.
|
||||||
|
//
|
||||||
|
// Note: NOT YET IMPLEMENTED.
|
||||||
|
func WithConsolef(f func(string, ...interface{})) BrowserOption {
|
||||||
|
return func(b *Browser) {
|
||||||
|
}
|
||||||
|
}
|
581
chromedp.go
581
chromedp.go
@ -8,428 +8,285 @@ package chromedp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/cdp"
|
"github.com/chromedp/cdproto/css"
|
||||||
|
"github.com/chromedp/cdproto/dom"
|
||||||
"github.com/chromedp/chromedp/client"
|
"github.com/chromedp/cdproto/inspector"
|
||||||
"github.com/chromedp/chromedp/runner"
|
"github.com/chromedp/cdproto/log"
|
||||||
|
"github.com/chromedp/cdproto/page"
|
||||||
|
"github.com/chromedp/cdproto/runtime"
|
||||||
|
"github.com/chromedp/cdproto/target"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// Context is attached to any context.Context which is valid for use with Run.
|
||||||
// DefaultNewTargetTimeout is the default time to wait for a new target to
|
type Context struct {
|
||||||
// be started.
|
// Allocator is used to create new browsers. It is inherited from the
|
||||||
DefaultNewTargetTimeout = 3 * time.Second
|
// parent context when using NewContext.
|
||||||
|
Allocator Allocator
|
||||||
|
|
||||||
// DefaultCheckDuration is the default time to sleep between a check.
|
// Browser is the browser being used in the context. It is inherited
|
||||||
DefaultCheckDuration = 50 * time.Millisecond
|
// from the parent context when using NewContext.
|
||||||
|
Browser *Browser
|
||||||
|
|
||||||
// DefaultPoolStartPort is the default start port number.
|
// Target is the target to run actions (commands) against. It is not
|
||||||
DefaultPoolStartPort = 9000
|
// inherited from the parent context, and typically each context will
|
||||||
|
// have its own unique Target pointing to a separate browser tab (page).
|
||||||
|
Target *Target
|
||||||
|
|
||||||
// DefaultPoolEndPort is the default end port number.
|
// browserOpts holds the browser options passed to NewContext via
|
||||||
DefaultPoolEndPort = 10000
|
// WithBrowserOption, so that they can later be used when allocating a
|
||||||
)
|
// browser in Run.
|
||||||
|
browserOpts []BrowserOption
|
||||||
|
|
||||||
// CDP is the high-level Chrome DevTools Protocol browser manager, handling the
|
// cancel simply cancels the context that was used to start Browser.
|
||||||
// browser process runner, WebSocket clients, associated targets, and network,
|
// This is useful to stop all activity and avoid deadlocks if we detect
|
||||||
// page, and DOM events.
|
// that the browser was closed or happened to crash. Note that this
|
||||||
type CDP struct {
|
// cancel function doesn't do any waiting.
|
||||||
// r is the chrome runner.
|
cancel func()
|
||||||
r *runner.Runner
|
|
||||||
|
|
||||||
// opts are command line options to pass to a created runner.
|
// first records whether this context was the one that allocated
|
||||||
opts []runner.CommandLineOption
|
// Browser. This is important, because its cancellation will stop the
|
||||||
|
// entire browser handler, meaning that no further actions can be
|
||||||
|
// executed.
|
||||||
|
first bool
|
||||||
|
|
||||||
// watch is the channel for new client targets.
|
// wg allows waiting for a target to be closed on cancellation.
|
||||||
watch <-chan client.Target
|
wg sync.WaitGroup
|
||||||
|
|
||||||
// cur is the current active target's handler.
|
// cancelErr is the first error encountered when cancelling this
|
||||||
cur cdp.Executor
|
// context, for example if a browser's temporary user data directory
|
||||||
|
// couldn't be deleted.
|
||||||
// handlers is the active handlers.
|
cancelErr error
|
||||||
handlers []*TargetHandler
|
|
||||||
|
|
||||||
// handlerMap is the map of target IDs to its active handler.
|
|
||||||
handlerMap map[string]int
|
|
||||||
|
|
||||||
// logging funcs
|
|
||||||
logf, debugf, errf func(string, ...interface{})
|
|
||||||
|
|
||||||
sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and starts a new CDP instance.
|
// NewContext creates a chromedp context from the parent context. The parent
|
||||||
func New(ctxt context.Context, opts ...Option) (*CDP, error) {
|
// context's Allocator is inherited, defaulting to an ExecAllocator with
|
||||||
c := &CDP{
|
// DefaultExecAllocatorOptions.
|
||||||
handlers: make([]*TargetHandler, 0),
|
//
|
||||||
handlerMap: make(map[string]int),
|
// If the parent context contains an allocated Browser, the child context
|
||||||
logf: log.Printf,
|
// inherits it, and its first Run creates a new tab on that browser. Otherwise,
|
||||||
debugf: func(string, ...interface{}) {},
|
// its first Run will allocate a new browser.
|
||||||
errf: func(s string, v ...interface{}) { log.Printf("error: "+s, v...) },
|
//
|
||||||
|
// Cancelling the returned context will close a tab or an entire browser,
|
||||||
|
// depending on the logic described above. To cancel a context while checking
|
||||||
|
// for errors, see Cancel.
|
||||||
|
func NewContext(parent context.Context, opts ...ContextOption) (context.Context, context.CancelFunc) {
|
||||||
|
ctx, cancel := context.WithCancel(parent)
|
||||||
|
|
||||||
|
c := &Context{cancel: cancel, first: true}
|
||||||
|
if pc := FromContext(parent); pc != nil {
|
||||||
|
c.Allocator = pc.Allocator
|
||||||
|
c.Browser = pc.Browser
|
||||||
|
// don't inherit Target, so that NewContext can be used to
|
||||||
|
// create a new tab on the same browser.
|
||||||
|
|
||||||
|
c.first = c.Browser == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply options
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
if err := o(c); err != nil {
|
o(c)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
if c.Allocator == nil {
|
||||||
|
c.Allocator = setupExecAllocator(DefaultExecAllocatorOptions...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for supplied runner, if none then create one
|
ctx = context.WithValue(ctx, contextKey{}, c)
|
||||||
if c.r == nil && c.watch == nil {
|
c.wg.Add(1)
|
||||||
var err error
|
|
||||||
c.r, err = runner.Run(ctxt, c.opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// watch handlers
|
|
||||||
if c.watch == nil {
|
|
||||||
c.watch = c.r.Client().WatchPageTargets(ctxt)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for t := range c.watch {
|
<-ctx.Done()
|
||||||
if t == nil {
|
if c.first {
|
||||||
|
// This is the original browser tab, so the entire
|
||||||
|
// browser will already be cleaned up elsewhere.
|
||||||
|
c.wg.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go c.AddTarget(ctxt, t)
|
|
||||||
|
if c.Target == nil {
|
||||||
|
// This is a new tab, but we didn't create it and attach
|
||||||
|
// to it yet. Nothing to do.
|
||||||
|
c.wg.Done()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not the original browser tab; simply detach and close it.
|
||||||
|
// We need a new context, as ctx is cancelled; use a 1s timeout.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if id := c.Target.SessionID; id != "" {
|
||||||
|
action := target.DetachFromTarget().WithSessionID(id)
|
||||||
|
if err := action.Do(ctx, c.Browser); c.cancelErr == nil {
|
||||||
|
c.cancelErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if id := c.Target.TargetID; id != "" {
|
||||||
|
action := target.CloseTarget(id)
|
||||||
|
if ok, err := action.Do(ctx, c.Browser); c.cancelErr == nil {
|
||||||
|
if !ok && err == nil {
|
||||||
|
err = fmt.Errorf("could not close target %q", id)
|
||||||
|
}
|
||||||
|
c.cancelErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.wg.Done()
|
||||||
}()
|
}()
|
||||||
|
cancelWait := func() {
|
||||||
// TODO: fix this
|
cancel()
|
||||||
timeout := time.After(defaultNewTargetTimeout)
|
c.wg.Wait()
|
||||||
|
|
||||||
// wait until at least one target active
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
default:
|
|
||||||
c.RLock()
|
|
||||||
exists := c.cur != nil
|
|
||||||
c.RUnlock()
|
|
||||||
if exists {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fix this
|
|
||||||
time.Sleep(DefaultCheckDuration)
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
return nil, ctxt.Err()
|
|
||||||
|
|
||||||
case <-timeout:
|
|
||||||
return nil, errors.New("timeout waiting for initial target")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return ctx, cancelWait
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTarget adds a target using the supplied context.
|
type contextKey struct{}
|
||||||
func (c *CDP) AddTarget(ctxt context.Context, t client.Target) {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
|
|
||||||
// create target manager
|
// FromContext extracts the Context data stored inside a context.Context.
|
||||||
h, err := NewTargetHandler(t, c.logf, c.debugf, c.errf)
|
func FromContext(ctx context.Context) *Context {
|
||||||
|
c, _ := ctx.Value(contextKey{}).(*Context)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel cancels a chromedp context, waits for its resources to be cleaned up,
|
||||||
|
// and returns any error encountered during that process.
|
||||||
|
//
|
||||||
|
// Usually a "defer cancel()" will be enough for most use cases. This API is
|
||||||
|
// useful if you want to catch underlying cancel errors, such as when a
|
||||||
|
// temporary directory cannot be deleted.
|
||||||
|
func Cancel(ctx context.Context) error {
|
||||||
|
c := FromContext(ctx)
|
||||||
|
if c == nil {
|
||||||
|
return ErrInvalidContext
|
||||||
|
}
|
||||||
|
c.cancel()
|
||||||
|
c.wg.Wait()
|
||||||
|
return c.cancelErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs an action against context. The provided context must be a valid
|
||||||
|
// chromedp context, typically created via NewContext.
|
||||||
|
func Run(ctx context.Context, actions ...Action) error {
|
||||||
|
c := FromContext(ctx)
|
||||||
|
// If c is nil, it's not a chromedp context.
|
||||||
|
// If c.Allocator is nil, NewContext wasn't used properly.
|
||||||
|
// If c.cancel is nil, Run is being called directly with an allocator
|
||||||
|
// context.
|
||||||
|
if c == nil || c.Allocator == nil || c.cancel == nil {
|
||||||
|
return ErrInvalidContext
|
||||||
|
}
|
||||||
|
if c.Browser == nil {
|
||||||
|
browser, err := c.Allocator.Allocate(ctx, c.browserOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.errf("could not create handler for %s: %v", t, err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
c.Browser = browser
|
||||||
// run
|
|
||||||
if err := h.Run(ctxt); err != nil {
|
|
||||||
c.errf("could not start handler for %s: %v", t, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if c.Target == nil {
|
||||||
// add to active handlers
|
if err := c.newSession(ctx); err != nil {
|
||||||
c.handlers = append(c.handlers, h)
|
return err
|
||||||
c.handlerMap[t.GetID()] = len(c.handlers) - 1
|
|
||||||
if c.cur == nil {
|
|
||||||
c.cur = h
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return Tasks(actions).Do(ctx, c.Target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait waits for the Chrome runner to terminate.
|
func (c *Context) newSession(ctx context.Context) error {
|
||||||
func (c *CDP) Wait() error {
|
var targetID target.ID
|
||||||
c.RLock()
|
if c.first {
|
||||||
r := c.r
|
// If we just allocated this browser, and it has a single page
|
||||||
c.RUnlock()
|
// that's blank and not attached, use it.
|
||||||
|
infos, err := target.GetTargets().Do(ctx, c.Browser)
|
||||||
if r != nil {
|
|
||||||
return r.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown closes all Chrome page handlers.
|
|
||||||
func (c *CDP) Shutdown(ctxt context.Context, opts ...client.Option) error {
|
|
||||||
c.RLock()
|
|
||||||
defer c.RUnlock()
|
|
||||||
|
|
||||||
if c.r != nil {
|
|
||||||
return c.r.Shutdown(ctxt, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListTargets returns the target IDs of the managed targets.
|
|
||||||
func (c *CDP) ListTargets() []string {
|
|
||||||
c.RLock()
|
|
||||||
defer c.RUnlock()
|
|
||||||
|
|
||||||
i, targets := 0, make([]string, len(c.handlers))
|
|
||||||
for k := range c.handlerMap {
|
|
||||||
targets[i] = k
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
return targets
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHandlerByIndex retrieves the domains manager for the specified index.
|
|
||||||
func (c *CDP) GetHandlerByIndex(i int) cdp.Executor {
|
|
||||||
c.RLock()
|
|
||||||
defer c.RUnlock()
|
|
||||||
|
|
||||||
if i < 0 || i >= len(c.handlers) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.handlers[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHandlerByID retrieves the domains manager for the specified target ID.
|
|
||||||
func (c *CDP) GetHandlerByID(id string) cdp.Executor {
|
|
||||||
c.RLock()
|
|
||||||
defer c.RUnlock()
|
|
||||||
|
|
||||||
if i, ok := c.handlerMap[id]; ok {
|
|
||||||
return c.handlers[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHandler sets the active handler to the target with the specified index.
|
|
||||||
func (c *CDP) SetHandler(i int) error {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
|
|
||||||
if i < 0 || i >= len(c.handlers) {
|
|
||||||
return fmt.Errorf("no handler associated with target index %d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.cur = c.handlers[i]
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHandlerByID sets the active target to the target with the specified id.
|
|
||||||
func (c *CDP) SetHandlerByID(id string) error {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
|
|
||||||
if i, ok := c.handlerMap[id]; ok {
|
|
||||||
c.cur = c.handlers[i]
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("no handler associated with target id %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTarget creates a new target using supplied context and options, returning
|
|
||||||
// the id of the created target only after the target has been started for
|
|
||||||
// monitoring.
|
|
||||||
func (c *CDP) newTarget(ctxt context.Context, opts ...client.Option) (string, error) {
|
|
||||||
c.RLock()
|
|
||||||
cl := c.r.Client(opts...)
|
|
||||||
c.RUnlock()
|
|
||||||
|
|
||||||
// new page target
|
|
||||||
t, err := cl.NewPageTarget(ctxt)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
|
pages := 0
|
||||||
timeout := time.After(DefaultNewTargetTimeout)
|
for _, info := range infos {
|
||||||
|
if info.Type == "page" && info.URL == "about:blank" && !info.Attached {
|
||||||
for {
|
targetID = info.TargetID
|
||||||
select {
|
pages++
|
||||||
default:
|
}
|
||||||
var ok bool
|
}
|
||||||
id := t.GetID()
|
if pages > 1 {
|
||||||
c.RLock()
|
// Multiple blank pages; just in case, don't use any.
|
||||||
_, ok = c.handlerMap[id]
|
targetID = ""
|
||||||
c.RUnlock()
|
|
||||||
if ok {
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(DefaultCheckDuration)
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
return "", ctxt.Err()
|
|
||||||
|
|
||||||
case <-timeout:
|
|
||||||
return "", errors.New("timeout waiting for new target to be available")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// SetTarget is an action that sets the active Chrome handler to the specified
|
if targetID == "" {
|
||||||
// index i.
|
var err error
|
||||||
func (c *CDP) SetTarget(i int) Action {
|
targetID, err = target.CreateTarget("about:blank").Do(ctx, c.Browser)
|
||||||
return ActionFunc(func(context.Context, cdp.Executor) error {
|
if err != nil {
|
||||||
return c.SetHandler(i)
|
return err
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTargetByID is an action that sets the active Chrome handler to the handler
|
sessionID, err := target.AttachToTarget(targetID).Do(ctx, c.Browser)
|
||||||
// associated with the specified id.
|
|
||||||
func (c *CDP) SetTargetByID(id string) Action {
|
|
||||||
return ActionFunc(func(context.Context, cdp.Executor) error {
|
|
||||||
return c.SetHandlerByID(id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTarget is an action that creates a new Chrome target, and sets it as the
|
|
||||||
// active target.
|
|
||||||
func (c *CDP) NewTarget(id *string, opts ...client.Option) Action {
|
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
|
||||||
n, err := c.newTarget(ctxt, opts...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if id != nil {
|
c.Target = c.Browser.newExecutorForTarget(ctx, targetID, sessionID)
|
||||||
*id = n
|
|
||||||
|
// enable domains
|
||||||
|
for _, enable := range []Action{
|
||||||
|
log.Enable(),
|
||||||
|
runtime.Enable(),
|
||||||
|
// network.Enable(),
|
||||||
|
inspector.Enable(),
|
||||||
|
page.Enable(),
|
||||||
|
dom.Enable(),
|
||||||
|
css.Enable(),
|
||||||
|
} {
|
||||||
|
if err := enable.Do(ctx, c.Target); err != nil {
|
||||||
|
return fmt.Errorf("unable to execute %T: %v", enable, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseByIndex closes the Chrome target with specified index i.
|
// ContextOption is a context option.
|
||||||
func (c *CDP) CloseByIndex(i int) Action {
|
type ContextOption func(*Context)
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
|
||||||
return nil
|
// WithLogf is a shortcut for WithBrowserOption(WithBrowserLogf(f)).
|
||||||
})
|
func WithLogf(f func(string, ...interface{})) ContextOption {
|
||||||
|
return WithBrowserOption(WithBrowserLogf(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseByID closes the Chrome target with the specified id.
|
// WithErrorf is a shortcut for WithBrowserOption(WithBrowserErrorf(f)).
|
||||||
func (c *CDP) CloseByID(id string) Action {
|
func WithErrorf(f func(string, ...interface{})) ContextOption {
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return WithBrowserOption(WithBrowserErrorf(f))
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run executes the action against the current target using the supplied
|
// WithDebugf is a shortcut for WithBrowserOption(WithBrowserDebugf(f)).
|
||||||
// context.
|
func WithDebugf(f func(string, ...interface{})) ContextOption {
|
||||||
func (c *CDP) Run(ctxt context.Context, a Action) error {
|
return WithBrowserOption(WithBrowserDebugf(f))
|
||||||
c.RLock()
|
|
||||||
cur := c.cur
|
|
||||||
c.RUnlock()
|
|
||||||
|
|
||||||
return a.Do(ctxt, cur)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option is a Chrome DevTools Protocol option.
|
// WithBrowserOption allows passing a number of browser options to the allocator
|
||||||
type Option func(*CDP) error
|
// when allocating a new browser. As such, this context option can only be used
|
||||||
|
// when NewContext is allocating a new browser.
|
||||||
// WithRunner is a CDP option to specify the underlying Chrome runner to
|
func WithBrowserOption(opts ...BrowserOption) ContextOption {
|
||||||
// monitor for page handlers.
|
return func(c *Context) {
|
||||||
func WithRunner(r *runner.Runner) Option {
|
if !c.first {
|
||||||
return func(c *CDP) error {
|
panic("WithBrowserOption can only be used when allocating a new browser")
|
||||||
c.r = r
|
}
|
||||||
return nil
|
c.browserOpts = append(c.browserOpts, opts...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithTargets is a CDP option to specify the incoming targets to monitor for
|
// Targets lists all the targets in the browser attached to the given context.
|
||||||
// page handlers.
|
func Targets(ctx context.Context) ([]*target.Info, error) {
|
||||||
func WithTargets(watch <-chan client.Target) Option {
|
// Don't rely on Run, as that needs to be able to call Targets, and we
|
||||||
return func(c *CDP) error {
|
// don't want cyclic func calls.
|
||||||
c.watch = watch
|
c := FromContext(ctx)
|
||||||
return nil
|
if c == nil || c.Allocator == nil {
|
||||||
|
return nil, ErrInvalidContext
|
||||||
}
|
}
|
||||||
}
|
if c.Browser == nil {
|
||||||
|
browser, err := c.Allocator.Allocate(ctx, c.browserOpts...)
|
||||||
// WithClient is a CDP option to use the incoming targets from a client.
|
if err != nil {
|
||||||
func WithClient(ctxt context.Context, cl *client.Client) Option {
|
return nil, err
|
||||||
return func(c *CDP) error {
|
|
||||||
return WithTargets(cl.WatchPageTargets(ctxt))(c)
|
|
||||||
}
|
}
|
||||||
}
|
c.Browser = browser
|
||||||
|
|
||||||
// WithURL is a CDP option to use a client with the specified URL.
|
|
||||||
func WithURL(ctxt context.Context, urlstr string) Option {
|
|
||||||
return func(c *CDP) error {
|
|
||||||
return WithClient(ctxt, client.New(client.URL(urlstr)))(c)
|
|
||||||
}
|
}
|
||||||
|
return target.GetTargets().Do(ctx, c.Browser)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithRunnerOptions is a CDP option to specify the options to pass to a newly
|
|
||||||
// created Chrome process runner.
|
|
||||||
func WithRunnerOptions(opts ...runner.CommandLineOption) Option {
|
|
||||||
return func(c *CDP) error {
|
|
||||||
c.opts = opts
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLogf is a CDP option to specify a func to receive general logging.
|
|
||||||
func WithLogf(f func(string, ...interface{})) Option {
|
|
||||||
return func(c *CDP) error {
|
|
||||||
c.logf = f
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDebugf is a CDP option to specify a func to receive debug logging (ie,
|
|
||||||
// protocol information).
|
|
||||||
func WithDebugf(f func(string, ...interface{})) Option {
|
|
||||||
return func(c *CDP) error {
|
|
||||||
c.debugf = f
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithErrorf is a CDP option to specify a func to receive error logging.
|
|
||||||
func WithErrorf(f func(string, ...interface{})) Option {
|
|
||||||
return func(c *CDP) error {
|
|
||||||
c.errf = f
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLog is a CDP option that sets the logging, debugging, and error funcs to
|
|
||||||
// f.
|
|
||||||
func WithLog(f func(string, ...interface{})) Option {
|
|
||||||
return func(c *CDP) error {
|
|
||||||
c.logf, c.debugf, c.errf = f, f, f
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithConsolef is a CDP option to specify a func to receive chrome log events.
|
|
||||||
//
|
|
||||||
// Note: NOT YET IMPLEMENTED.
|
|
||||||
func WithConsolef(f func(string, ...interface{})) Option {
|
|
||||||
return func(c *CDP) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// defaultNewTargetTimeout is the default target timeout -- used by
|
|
||||||
// testing.
|
|
||||||
defaultNewTargetTimeout = DefaultNewTargetTimeout
|
|
||||||
)
|
|
||||||
|
263
chromedp_test.go
263
chromedp_test.go
@ -2,117 +2,226 @@ package chromedp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/chromedp/chromedp/runner"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pool *Pool
|
|
||||||
testdataDir string
|
testdataDir string
|
||||||
|
|
||||||
defaultContext, defaultCancel = context.WithCancel(context.Background())
|
browserCtx context.Context
|
||||||
|
|
||||||
cliOpts = []runner.CommandLineOption{
|
// allocOpts is filled in TestMain
|
||||||
runner.NoDefaultBrowserCheck,
|
allocOpts []ExecAllocatorOption
|
||||||
runner.NoFirstRun,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testAllocate(t *testing.T, path string) *Res {
|
func testAllocate(t *testing.T, path string) (_ context.Context, cancel func()) {
|
||||||
c, err := pool.Allocate(defaultContext, cliOpts...)
|
// Same browser, new tab; not needing to start new chrome browsers for
|
||||||
if err != nil {
|
// each test gives a huge speed-up.
|
||||||
t.Fatalf("could not allocate from pool: %v", err)
|
ctx, _ := NewContext(browserCtx)
|
||||||
}
|
|
||||||
|
|
||||||
err = WithLogf(t.Logf)(c.c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not set logf: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = WithDebugf(t.Logf)(c.c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not set debugf: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = WithErrorf(t.Errorf)(c.c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not set errorf: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
h := c.c.GetHandlerByIndex(0)
|
|
||||||
th, ok := h.(*TargetHandler)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("handler is invalid type")
|
|
||||||
}
|
|
||||||
|
|
||||||
th.logf, th.debugf = t.Logf, t.Logf
|
|
||||||
th.errf = func(s string, v ...interface{}) {
|
|
||||||
t.Logf("TARGET HANDLER ERROR: "+s, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Only navigate if we want a path, otherwise leave the blank page.
|
||||||
if path != "" {
|
if path != "" {
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/"+path))
|
if err := Run(ctx, Navigate(testdataDir+"/"+path)); err != nil {
|
||||||
if err != nil {
|
t.Fatal(err)
|
||||||
t.Fatalf("could not navigate to testdata/%s: %v", path, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
cancelErr := func() {
|
||||||
|
if err := Cancel(ctx); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx, cancelErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
var err error
|
|
||||||
|
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("could not get working directory: %v", err)
|
panic(fmt.Sprintf("could not get working directory: %v", err))
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
testdataDir = "file://" + path.Join(wd, "testdata")
|
testdataDir = "file://" + path.Join(wd, "testdata")
|
||||||
|
|
||||||
// its worth noting that newer versions of chrome (64+) run much faster
|
// build on top of the default options
|
||||||
// than older ones -- same for headless_shell ...
|
allocOpts = append(allocOpts, DefaultExecAllocatorOptions...)
|
||||||
execPath := os.Getenv("CHROMEDP_TEST_RUNNER")
|
|
||||||
if execPath == "" {
|
|
||||||
execPath = runner.LookChromeNames("headless_shell")
|
|
||||||
}
|
|
||||||
cliOpts = append(cliOpts, runner.ExecPath(execPath))
|
|
||||||
|
|
||||||
|
// disabling the GPU helps portability with some systems like Travis,
|
||||||
|
// and can slightly speed up the tests on other systems
|
||||||
|
allocOpts = append(allocOpts, DisableGPU)
|
||||||
|
|
||||||
|
// it's worth noting that newer versions of chrome (64+) run much faster
|
||||||
|
// than older ones -- same for headless_shell ...
|
||||||
|
if execPath := os.Getenv("CHROMEDP_TEST_RUNNER"); execPath != "" {
|
||||||
|
allocOpts = append(allocOpts, ExecPath(execPath))
|
||||||
|
}
|
||||||
// not explicitly needed to be set, as this vastly speeds up unit tests
|
// not explicitly needed to be set, as this vastly speeds up unit tests
|
||||||
if noSandbox := os.Getenv("CHROMEDP_NO_SANDBOX"); noSandbox != "false" {
|
if noSandbox := os.Getenv("CHROMEDP_NO_SANDBOX"); noSandbox != "false" {
|
||||||
cliOpts = append(cliOpts, runner.NoSandbox)
|
allocOpts = append(allocOpts, NoSandbox)
|
||||||
}
|
|
||||||
// must be explicitly set, as disabling gpu slows unit tests
|
|
||||||
if disableGPU := os.Getenv("CHROMEDP_DISABLE_GPU"); disableGPU != "" && disableGPU != "false" {
|
|
||||||
cliOpts = append(cliOpts, runner.DisableGPU)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetTimeout := os.Getenv("CHROMEDP_TARGET_TIMEOUT"); targetTimeout != "" {
|
allocCtx, cancel := NewExecAllocator(context.Background(), allocOpts...)
|
||||||
defaultNewTargetTimeout, _ = time.ParseDuration(targetTimeout)
|
|
||||||
}
|
|
||||||
if defaultNewTargetTimeout == 0 {
|
|
||||||
defaultNewTargetTimeout = 30 * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
//pool, err = NewPool(PoolLog(log.Printf, log.Printf, log.Printf))
|
// start the browser
|
||||||
pool, err = NewPool()
|
browserCtx, _ = NewContext(allocCtx)
|
||||||
if err != nil {
|
if err := Run(browserCtx); err != nil {
|
||||||
log.Fatal(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
|
|
||||||
defaultCancel()
|
cancel()
|
||||||
|
|
||||||
err = pool.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTargets(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Start one browser with one tab.
|
||||||
|
ctx1, cancel1 := NewContext(context.Background())
|
||||||
|
defer cancel1()
|
||||||
|
if err := Run(ctx1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantTargets := func(ctx context.Context, want int) {
|
||||||
|
t.Helper()
|
||||||
|
infos, err := Targets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := len(infos); want != got {
|
||||||
|
t.Fatalf("want %d targets, got %d", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wantTargets(ctx1, 1)
|
||||||
|
|
||||||
|
// Start a second tab on the same browser.
|
||||||
|
ctx2, cancel2 := NewContext(ctx1)
|
||||||
|
defer cancel2()
|
||||||
|
if err := Run(ctx2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantTargets(ctx2, 2)
|
||||||
|
|
||||||
|
// The first context should also see both targets.
|
||||||
|
wantTargets(ctx1, 2)
|
||||||
|
|
||||||
|
// Cancelling the second context should close the second tab alone.
|
||||||
|
cancel2()
|
||||||
|
wantTargets(ctx1, 1)
|
||||||
|
|
||||||
|
// We used to have a bug where Run would reset the first context as if
|
||||||
|
// it weren't the first, breaking its cancellation.
|
||||||
|
if err := Run(ctx1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBrowserQuit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("os.Interrupt isn't supported on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate a scenario where we navigate to a page that's slow to
|
||||||
|
// respond, and the browser is closed before we can finish the
|
||||||
|
// navigation.
|
||||||
|
serve := make(chan bool, 1)
|
||||||
|
close := make(chan bool, 1)
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
close <- true
|
||||||
|
<-serve
|
||||||
|
fmt.Fprintf(w, "response")
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
ctx, cancel := NewContext(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
if err := Run(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-close
|
||||||
|
b := FromContext(ctx).Browser
|
||||||
|
if err := b.process.Signal(os.Interrupt); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
serve <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Run should error with something other than "deadline exceeded" in
|
||||||
|
// much less than 5s.
|
||||||
|
ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
switch err := Run(ctx2, Navigate(s.URL)); err {
|
||||||
|
case nil:
|
||||||
|
t.Fatal("did not expect a nil error")
|
||||||
|
case context.DeadlineExceeded:
|
||||||
|
t.Fatalf("did not expect a standard context error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCancelError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx1, cancel1 := NewContext(context.Background())
|
||||||
|
defer cancel1()
|
||||||
|
if err := Run(ctx1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open and close a target normally; no error.
|
||||||
|
ctx2, cancel2 := NewContext(ctx1)
|
||||||
|
defer cancel2()
|
||||||
|
if err := Run(ctx2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := Cancel(ctx2); err != nil {
|
||||||
|
t.Fatalf("expected a nil error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make "cancel" close the wrong target; error.
|
||||||
|
ctx3, cancel3 := NewContext(ctx1)
|
||||||
|
defer cancel3()
|
||||||
|
if err := Run(ctx3); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
FromContext(ctx3).Target.TargetID = "wrong"
|
||||||
|
if err := Cancel(ctx3); err == nil {
|
||||||
|
t.Fatalf("expected a non-nil error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrematureCancel(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Cancel before the browser is allocated.
|
||||||
|
ctx, cancel := NewContext(context.Background())
|
||||||
|
cancel()
|
||||||
|
if err := Run(ctx); err != context.Canceled {
|
||||||
|
t.Fatalf("wanted canceled context error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrematureCancelTab(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx1, cancel := NewContext(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
if err := Run(ctx1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel after the browser is allocated, but before we've created a new
|
||||||
|
// tab.
|
||||||
|
ctx2, cancel := NewContext(ctx1)
|
||||||
|
cancel()
|
||||||
|
Run(ctx2)
|
||||||
|
}
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
//go:generate easyjson -omit_empty -output_filename easyjson.go chrome.go
|
|
||||||
|
|
||||||
// Chrome holds connection information for a Chrome target.
|
|
||||||
//
|
|
||||||
//easyjson:json
|
|
||||||
type Chrome struct {
|
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
DevtoolsURL string `json:"devtoolsFrontendUrl,omitempty"`
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
Type TargetType `json:"type,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
WebsocketURL string `json:"webSocketDebuggerUrl,omitempty"`
|
|
||||||
FaviconURL string `json:"faviconURL,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// String satisfies the stringer interface.
|
|
||||||
func (c Chrome) String() string {
|
|
||||||
return fmt.Sprintf("[%s]: %q", c.ID, c.Title)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetID returns the target ID.
|
|
||||||
func (c *Chrome) GetID() string {
|
|
||||||
return c.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetType returns the target type.
|
|
||||||
func (c *Chrome) GetType() TargetType {
|
|
||||||
return c.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDevtoolsURL returns the devtools frontend target URL, satisfying the
|
|
||||||
// domains.Target interface.
|
|
||||||
func (c *Chrome) GetDevtoolsURL() string {
|
|
||||||
return c.DevtoolsURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWebsocketURL provides the websocket URL for the target, satisfying the
|
|
||||||
// domains.Target interface.
|
|
||||||
func (c *Chrome) GetWebsocketURL() string {
|
|
||||||
return c.WebsocketURL
|
|
||||||
}
|
|
340
client/client.go
340
client/client.go
@ -1,340 +0,0 @@
|
|||||||
// Package client provides the low level Chrome DevTools Protocol client.
|
|
||||||
package client
|
|
||||||
|
|
||||||
//go:generate go run gen.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mailru/easyjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultEndpoint is the default endpoint to connect to.
|
|
||||||
DefaultEndpoint = "http://localhost:9222/json"
|
|
||||||
|
|
||||||
// DefaultWatchInterval is the default check duration.
|
|
||||||
DefaultWatchInterval = 100 * time.Millisecond
|
|
||||||
|
|
||||||
// DefaultWatchTimeout is the default watch timeout.
|
|
||||||
DefaultWatchTimeout = 5 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error is a client error.
|
|
||||||
type Error string
|
|
||||||
|
|
||||||
// Error satisfies the error interface.
|
|
||||||
func (err Error) Error() string {
|
|
||||||
return string(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrUnsupportedProtocolType is the unsupported protocol type error.
|
|
||||||
ErrUnsupportedProtocolType Error = "unsupported protocol type"
|
|
||||||
|
|
||||||
// ErrUnsupportedProtocolVersion is the unsupported protocol version error.
|
|
||||||
ErrUnsupportedProtocolVersion Error = "unsupported protocol version"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Target is the common interface for a Chrome DevTools Protocol target.
|
|
||||||
type Target interface {
|
|
||||||
String() string
|
|
||||||
GetID() string
|
|
||||||
GetType() TargetType
|
|
||||||
GetDevtoolsURL() string
|
|
||||||
GetWebsocketURL() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client is a Chrome DevTools Protocol client.
|
|
||||||
type Client struct {
|
|
||||||
url string
|
|
||||||
check time.Duration
|
|
||||||
timeout time.Duration
|
|
||||||
|
|
||||||
ver, typ string
|
|
||||||
rw sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new Chrome DevTools Protocol client.
|
|
||||||
func New(opts ...Option) *Client {
|
|
||||||
c := &Client{
|
|
||||||
url: DefaultEndpoint,
|
|
||||||
check: DefaultWatchInterval,
|
|
||||||
timeout: DefaultWatchTimeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply opts
|
|
||||||
for _, o := range opts {
|
|
||||||
o(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// doReq executes a request.
|
|
||||||
func (c *Client) doReq(ctxt context.Context, action string, v interface{}) error {
|
|
||||||
// create request
|
|
||||||
req, err := http.NewRequest("GET", c.url+"/"+action, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req = req.WithContext(ctxt)
|
|
||||||
|
|
||||||
cl := &http.Client{}
|
|
||||||
|
|
||||||
// execute
|
|
||||||
res, err := cl.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if v != nil {
|
|
||||||
// load body
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// unmarshal
|
|
||||||
if z, ok := v.(easyjson.Unmarshaler); ok {
|
|
||||||
return easyjson.Unmarshal(body, z)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Unmarshal(body, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListTargets returns a list of all targets.
|
|
||||||
func (c *Client) ListTargets(ctxt context.Context) ([]Target, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
var l []json.RawMessage
|
|
||||||
if err = c.doReq(ctxt, "list", &l); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t := make([]Target, len(l))
|
|
||||||
for i, v := range l {
|
|
||||||
t[i], err = c.newTarget(ctxt, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListTargetsWithType returns a list of Targets with the specified target
|
|
||||||
// type.
|
|
||||||
func (c *Client) ListTargetsWithType(ctxt context.Context, typ TargetType) ([]Target, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
targets, err := c.ListTargets(ctxt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret []Target
|
|
||||||
for _, t := range targets {
|
|
||||||
if t.GetType() == typ {
|
|
||||||
ret = append(ret, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPageTargets lists the available Page targets.
|
|
||||||
func (c *Client) ListPageTargets(ctxt context.Context) ([]Target, error) {
|
|
||||||
return c.ListTargetsWithType(ctxt, Page)
|
|
||||||
}
|
|
||||||
|
|
||||||
var browserRE = regexp.MustCompile(`(?i)^(chrome|chromium|microsoft edge|safari)`)
|
|
||||||
|
|
||||||
// loadProtocolInfo loads the protocol information from the remote URL.
|
|
||||||
func (c *Client) loadProtocolInfo(ctxt context.Context) (string, string, error) {
|
|
||||||
c.rw.Lock()
|
|
||||||
defer c.rw.Unlock()
|
|
||||||
|
|
||||||
if c.ver == "" || c.typ == "" {
|
|
||||||
v, err := c.VersionInfo(ctxt)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if m := browserRE.FindAllStringSubmatch(v["Browser"], -1); len(m) != 0 {
|
|
||||||
c.typ = strings.ToLower(m[0][0])
|
|
||||||
}
|
|
||||||
c.ver = v["Protocol-Version"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.ver, c.typ, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTarget creates a new target.
|
|
||||||
func (c *Client) newTarget(ctxt context.Context, buf []byte) (Target, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
ver, typ, err := c.loadProtocolInfo(ctxt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ver != "1.1" && ver != "1.2" && ver != "1.3" {
|
|
||||||
return nil, ErrUnsupportedProtocolVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
switch typ {
|
|
||||||
case "chrome", "chromium", "microsoft edge", "safari", "":
|
|
||||||
x := new(Chrome)
|
|
||||||
if buf != nil {
|
|
||||||
if err = easyjson.Unmarshal(buf, x); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return x, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrUnsupportedProtocolType
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPageTargetWithURL creates a new page target with the specified url.
|
|
||||||
func (c *Client) NewPageTargetWithURL(ctxt context.Context, urlstr string) (Target, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
t, err := c.newTarget(ctxt, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
u := "new"
|
|
||||||
if urlstr != "" {
|
|
||||||
u += "?" + urlstr
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.doReq(ctxt, u, t); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPageTarget creates a new page target.
|
|
||||||
func (c *Client) NewPageTarget(ctxt context.Context) (Target, error) {
|
|
||||||
return c.NewPageTargetWithURL(ctxt, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActivateTarget activates a target.
|
|
||||||
func (c *Client) ActivateTarget(ctxt context.Context, t Target) error {
|
|
||||||
return c.doReq(ctxt, "activate/"+t.GetID(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseTarget activates a target.
|
|
||||||
func (c *Client) CloseTarget(ctxt context.Context, t Target) error {
|
|
||||||
return c.doReq(ctxt, "close/"+t.GetID(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VersionInfo returns information about the remote debugging protocol.
|
|
||||||
func (c *Client) VersionInfo(ctxt context.Context) (map[string]string, error) {
|
|
||||||
v := make(map[string]string)
|
|
||||||
if err := c.doReq(ctxt, "version", &v); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatchPageTargets watches for new page targets.
|
|
||||||
func (c *Client) WatchPageTargets(ctxt context.Context) <-chan Target {
|
|
||||||
ch := make(chan Target)
|
|
||||||
go func() {
|
|
||||||
defer close(ch)
|
|
||||||
|
|
||||||
encountered := make(map[string]bool)
|
|
||||||
check := func() error {
|
|
||||||
targets, err := c.ListPageTargets(ctxt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range targets {
|
|
||||||
if !encountered[t.GetID()] {
|
|
||||||
ch <- t
|
|
||||||
}
|
|
||||||
encountered[t.GetID()] = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
lastGood := time.Now()
|
|
||||||
for {
|
|
||||||
err = check()
|
|
||||||
if err == nil {
|
|
||||||
lastGood = time.Now()
|
|
||||||
} else if time.Now().After(lastGood.Add(c.timeout)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(c.check):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option is a Chrome DevTools Protocol client option.
|
|
||||||
type Option func(*Client)
|
|
||||||
|
|
||||||
// URL is a client option to specify the remote Chrome DevTools Protocol
|
|
||||||
// instance to connect to.
|
|
||||||
func URL(urlstr string) Option {
|
|
||||||
return func(c *Client) {
|
|
||||||
// since chrome 66+, dev tools requires the host name to be either an
|
|
||||||
// IP address, or "localhost"
|
|
||||||
if strings.HasPrefix(strings.ToLower(urlstr), "http://") {
|
|
||||||
host, port, path := urlstr[7:], "", ""
|
|
||||||
if i := strings.Index(host, "/"); i != -1 {
|
|
||||||
host, path = host[:i], host[i:]
|
|
||||||
}
|
|
||||||
if i := strings.Index(host, ":"); i != -1 {
|
|
||||||
host, port = host[:i], host[i:]
|
|
||||||
}
|
|
||||||
if addr, err := net.ResolveIPAddr("ip", host); err == nil {
|
|
||||||
urlstr = "http://" + addr.IP.String() + port + path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.url = urlstr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatchInterval is a client option that specifies the check interval duration.
|
|
||||||
func WatchInterval(check time.Duration) Option {
|
|
||||||
return func(c *Client) {
|
|
||||||
c.check = check
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatchTimeout is a client option that specifies the watch timeout duration.
|
|
||||||
func WatchTimeout(timeout time.Duration) Option {
|
|
||||||
return func(c *Client) {
|
|
||||||
c.timeout = timeout
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,174 +0,0 @@
|
|||||||
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
json "encoding/json"
|
|
||||||
easyjson "github.com/mailru/easyjson"
|
|
||||||
jlexer "github.com/mailru/easyjson/jlexer"
|
|
||||||
jwriter "github.com/mailru/easyjson/jwriter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// suppress unused package warning
|
|
||||||
var (
|
|
||||||
_ *json.RawMessage
|
|
||||||
_ *jlexer.Lexer
|
|
||||||
_ *jwriter.Writer
|
|
||||||
_ easyjson.Marshaler
|
|
||||||
)
|
|
||||||
|
|
||||||
func easyjsonC5a4559bDecodeGithubComChromedpChromedpClient(in *jlexer.Lexer, out *Chrome) {
|
|
||||||
isTopLevel := in.IsStart()
|
|
||||||
if in.IsNull() {
|
|
||||||
if isTopLevel {
|
|
||||||
in.Consumed()
|
|
||||||
}
|
|
||||||
in.Skip()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
in.Delim('{')
|
|
||||||
for !in.IsDelim('}') {
|
|
||||||
key := in.UnsafeString()
|
|
||||||
in.WantColon()
|
|
||||||
if in.IsNull() {
|
|
||||||
in.Skip()
|
|
||||||
in.WantComma()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch key {
|
|
||||||
case "description":
|
|
||||||
out.Description = string(in.String())
|
|
||||||
case "devtoolsFrontendUrl":
|
|
||||||
out.DevtoolsURL = string(in.String())
|
|
||||||
case "id":
|
|
||||||
out.ID = string(in.String())
|
|
||||||
case "title":
|
|
||||||
out.Title = string(in.String())
|
|
||||||
case "type":
|
|
||||||
(out.Type).UnmarshalEasyJSON(in)
|
|
||||||
case "url":
|
|
||||||
out.URL = string(in.String())
|
|
||||||
case "webSocketDebuggerUrl":
|
|
||||||
out.WebsocketURL = string(in.String())
|
|
||||||
case "faviconURL":
|
|
||||||
out.FaviconURL = string(in.String())
|
|
||||||
default:
|
|
||||||
in.SkipRecursive()
|
|
||||||
}
|
|
||||||
in.WantComma()
|
|
||||||
}
|
|
||||||
in.Delim('}')
|
|
||||||
if isTopLevel {
|
|
||||||
in.Consumed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func easyjsonC5a4559bEncodeGithubComChromedpChromedpClient(out *jwriter.Writer, in Chrome) {
|
|
||||||
out.RawByte('{')
|
|
||||||
first := true
|
|
||||||
_ = first
|
|
||||||
if in.Description != "" {
|
|
||||||
const prefix string = ",\"description\":"
|
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
out.RawString(prefix[1:])
|
|
||||||
} else {
|
|
||||||
out.RawString(prefix)
|
|
||||||
}
|
|
||||||
out.String(string(in.Description))
|
|
||||||
}
|
|
||||||
if in.DevtoolsURL != "" {
|
|
||||||
const prefix string = ",\"devtoolsFrontendUrl\":"
|
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
out.RawString(prefix[1:])
|
|
||||||
} else {
|
|
||||||
out.RawString(prefix)
|
|
||||||
}
|
|
||||||
out.String(string(in.DevtoolsURL))
|
|
||||||
}
|
|
||||||
if in.ID != "" {
|
|
||||||
const prefix string = ",\"id\":"
|
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
out.RawString(prefix[1:])
|
|
||||||
} else {
|
|
||||||
out.RawString(prefix)
|
|
||||||
}
|
|
||||||
out.String(string(in.ID))
|
|
||||||
}
|
|
||||||
if in.Title != "" {
|
|
||||||
const prefix string = ",\"title\":"
|
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
out.RawString(prefix[1:])
|
|
||||||
} else {
|
|
||||||
out.RawString(prefix)
|
|
||||||
}
|
|
||||||
out.String(string(in.Title))
|
|
||||||
}
|
|
||||||
if in.Type != "" {
|
|
||||||
const prefix string = ",\"type\":"
|
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
out.RawString(prefix[1:])
|
|
||||||
} else {
|
|
||||||
out.RawString(prefix)
|
|
||||||
}
|
|
||||||
(in.Type).MarshalEasyJSON(out)
|
|
||||||
}
|
|
||||||
if in.URL != "" {
|
|
||||||
const prefix string = ",\"url\":"
|
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
out.RawString(prefix[1:])
|
|
||||||
} else {
|
|
||||||
out.RawString(prefix)
|
|
||||||
}
|
|
||||||
out.String(string(in.URL))
|
|
||||||
}
|
|
||||||
if in.WebsocketURL != "" {
|
|
||||||
const prefix string = ",\"webSocketDebuggerUrl\":"
|
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
out.RawString(prefix[1:])
|
|
||||||
} else {
|
|
||||||
out.RawString(prefix)
|
|
||||||
}
|
|
||||||
out.String(string(in.WebsocketURL))
|
|
||||||
}
|
|
||||||
if in.FaviconURL != "" {
|
|
||||||
const prefix string = ",\"faviconURL\":"
|
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
out.RawString(prefix[1:])
|
|
||||||
} else {
|
|
||||||
out.RawString(prefix)
|
|
||||||
}
|
|
||||||
out.String(string(in.FaviconURL))
|
|
||||||
}
|
|
||||||
out.RawByte('}')
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON supports json.Marshaler interface
|
|
||||||
func (v Chrome) MarshalJSON() ([]byte, error) {
|
|
||||||
w := jwriter.Writer{}
|
|
||||||
easyjsonC5a4559bEncodeGithubComChromedpChromedpClient(&w, v)
|
|
||||||
return w.Buffer.BuildBytes(), w.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
|
||||||
func (v Chrome) MarshalEasyJSON(w *jwriter.Writer) {
|
|
||||||
easyjsonC5a4559bEncodeGithubComChromedpChromedpClient(w, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON supports json.Unmarshaler interface
|
|
||||||
func (v *Chrome) UnmarshalJSON(data []byte) error {
|
|
||||||
r := jlexer.Lexer{Data: data}
|
|
||||||
easyjsonC5a4559bDecodeGithubComChromedpChromedpClient(&r, v)
|
|
||||||
return r.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
|
||||||
func (v *Chrome) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
|
||||||
easyjsonC5a4559bDecodeGithubComChromedpChromedpClient(l, v)
|
|
||||||
}
|
|
145
client/gen.go
145
client/gen.go
@ -1,145 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/knq/snaker"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// chromiumSrc is the base chromium source repo location
|
|
||||||
chromiumSrc = "https://chromium.googlesource.com/chromium/src"
|
|
||||||
|
|
||||||
// devtoolsHTTPClientCc contains the target_type names.
|
|
||||||
devtoolsHTTPClientCc = chromiumSrc + "/+/master/chrome/test/chromedriver/chrome/devtools_http_client.cc?format=TEXT"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
flagOut = flag.String("out", "targettype.go", "out file")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if err := run(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var typeAsStringRE = regexp.MustCompile(`type_as_string\s+==\s+"([^"]+)"`)
|
|
||||||
|
|
||||||
// run executes the generator.
|
|
||||||
func run() error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// grab source
|
|
||||||
buf, err := grab(devtoolsHTTPClientCc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// find names
|
|
||||||
matches := typeAsStringRE.FindAllStringSubmatch(string(buf), -1)
|
|
||||||
names := make([]string, len(matches))
|
|
||||||
for i, m := range matches {
|
|
||||||
names[i] = m[1]
|
|
||||||
}
|
|
||||||
sort.Strings(names)
|
|
||||||
|
|
||||||
// process names
|
|
||||||
var constVals, decodeVals string
|
|
||||||
for _, n := range names {
|
|
||||||
name := snaker.SnakeToCamelIdentifier(n)
|
|
||||||
constVals += fmt.Sprintf("%s TargetType = \"%s\"\n", name, n)
|
|
||||||
decodeVals += fmt.Sprintf("case %s:\n*tt=%s\n", name, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = ioutil.WriteFile(*flagOut, []byte(fmt.Sprintf(targetTypeSrc, constVals, decodeVals)), 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return exec.Command("gofmt", "-w", "-s", *flagOut).Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// grab retrieves a file from the chromium source code.
|
|
||||||
func grab(path string) ([]byte, error) {
|
|
||||||
res, err := http.Get(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := base64.StdEncoding.DecodeString(string(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
targetTypeSrc = `package client
|
|
||||||
|
|
||||||
// Code generated by gen.go. DO NOT EDIT.
|
|
||||||
|
|
||||||
import (
|
|
||||||
easyjson "github.com/mailru/easyjson"
|
|
||||||
jlexer "github.com/mailru/easyjson/jlexer"
|
|
||||||
jwriter "github.com/mailru/easyjson/jwriter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TargetType are the types of targets available in Chrome.
|
|
||||||
type TargetType string
|
|
||||||
|
|
||||||
// TargetType values.
|
|
||||||
const (
|
|
||||||
%s
|
|
||||||
)
|
|
||||||
|
|
||||||
// String satisfies stringer.
|
|
||||||
func (tt TargetType) String() string {
|
|
||||||
return string(tt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalEasyJSON satisfies easyjson.Marshaler.
|
|
||||||
func (tt TargetType) MarshalEasyJSON(out *jwriter.Writer) {
|
|
||||||
out.String(string(tt))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON satisfies json.Marshaler.
|
|
||||||
func (tt TargetType) MarshalJSON() ([]byte, error) {
|
|
||||||
return easyjson.Marshal(tt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalEasyJSON satisfies easyjson.Unmarshaler.
|
|
||||||
func (tt *TargetType) UnmarshalEasyJSON(in *jlexer.Lexer) {
|
|
||||||
z := TargetType(in.String())
|
|
||||||
switch z {
|
|
||||||
%s
|
|
||||||
|
|
||||||
default:
|
|
||||||
*tt = z
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON satisfies json.Unmarshaler.
|
|
||||||
func (tt *TargetType) UnmarshalJSON(buf []byte) error {
|
|
||||||
return easyjson.Unmarshal(buf, tt)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
@ -1,79 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
// Code generated by gen.go. DO NOT EDIT.
|
|
||||||
|
|
||||||
import (
|
|
||||||
easyjson "github.com/mailru/easyjson"
|
|
||||||
jlexer "github.com/mailru/easyjson/jlexer"
|
|
||||||
jwriter "github.com/mailru/easyjson/jwriter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TargetType are the types of targets available in Chrome.
|
|
||||||
type TargetType string
|
|
||||||
|
|
||||||
// TargetType values.
|
|
||||||
const (
|
|
||||||
App TargetType = "app"
|
|
||||||
BackgroundPage TargetType = "background_page"
|
|
||||||
Browser TargetType = "browser"
|
|
||||||
External TargetType = "external"
|
|
||||||
Iframe TargetType = "iframe"
|
|
||||||
Other TargetType = "other"
|
|
||||||
Page TargetType = "page"
|
|
||||||
ServiceWorker TargetType = "service_worker"
|
|
||||||
SharedWorker TargetType = "shared_worker"
|
|
||||||
Webview TargetType = "webview"
|
|
||||||
Worker TargetType = "worker"
|
|
||||||
)
|
|
||||||
|
|
||||||
// String satisfies stringer.
|
|
||||||
func (tt TargetType) String() string {
|
|
||||||
return string(tt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalEasyJSON satisfies easyjson.Marshaler.
|
|
||||||
func (tt TargetType) MarshalEasyJSON(out *jwriter.Writer) {
|
|
||||||
out.String(string(tt))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON satisfies json.Marshaler.
|
|
||||||
func (tt TargetType) MarshalJSON() ([]byte, error) {
|
|
||||||
return easyjson.Marshal(tt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalEasyJSON satisfies easyjson.Unmarshaler.
|
|
||||||
func (tt *TargetType) UnmarshalEasyJSON(in *jlexer.Lexer) {
|
|
||||||
z := TargetType(in.String())
|
|
||||||
switch z {
|
|
||||||
case App:
|
|
||||||
*tt = App
|
|
||||||
case BackgroundPage:
|
|
||||||
*tt = BackgroundPage
|
|
||||||
case Browser:
|
|
||||||
*tt = Browser
|
|
||||||
case External:
|
|
||||||
*tt = External
|
|
||||||
case Iframe:
|
|
||||||
*tt = Iframe
|
|
||||||
case Other:
|
|
||||||
*tt = Other
|
|
||||||
case Page:
|
|
||||||
*tt = Page
|
|
||||||
case ServiceWorker:
|
|
||||||
*tt = ServiceWorker
|
|
||||||
case SharedWorker:
|
|
||||||
*tt = SharedWorker
|
|
||||||
case Webview:
|
|
||||||
*tt = Webview
|
|
||||||
case Worker:
|
|
||||||
*tt = Worker
|
|
||||||
|
|
||||||
default:
|
|
||||||
*tt = z
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON satisfies json.Unmarshaler.
|
|
||||||
func (tt *TargetType) UnmarshalJSON(buf []byte) error {
|
|
||||||
return easyjson.Unmarshal(buf, tt)
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// DefaultReadBufferSize is the default maximum read buffer size.
|
|
||||||
DefaultReadBufferSize = 25 * 1024 * 1024
|
|
||||||
|
|
||||||
// DefaultWriteBufferSize is the default maximum write buffer size.
|
|
||||||
DefaultWriteBufferSize = 10 * 1024 * 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
// Transport is the common interface to send/receive messages to a target.
|
|
||||||
type Transport interface {
|
|
||||||
Read() ([]byte, error)
|
|
||||||
Write([]byte) error
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conn wraps a gorilla/websocket.Conn connection.
|
|
||||||
type Conn struct {
|
|
||||||
*websocket.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads the next websocket message.
|
|
||||||
func (c *Conn) Read() ([]byte, error) {
|
|
||||||
_, buf, err := c.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes a websocket message.
|
|
||||||
func (c *Conn) Write(buf []byte) error {
|
|
||||||
return c.WriteMessage(websocket.TextMessage, buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial dials the specified target's websocket URL.
|
|
||||||
//
|
|
||||||
// Note: uses gorilla/websocket.
|
|
||||||
func Dial(urlstr string, opts ...DialOption) (Transport, error) {
|
|
||||||
d := &websocket.Dialer{
|
|
||||||
ReadBufferSize: DefaultReadBufferSize,
|
|
||||||
WriteBufferSize: DefaultWriteBufferSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply opts
|
|
||||||
for _, o := range opts {
|
|
||||||
o(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect
|
|
||||||
conn, _, err := d.Dial(urlstr, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Conn{conn}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialOption is a dial option.
|
|
||||||
type DialOption func(*websocket.Dialer)
|
|
152
conn.go
Normal file
152
conn.go
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
package chromedp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/mailru/easyjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultReadBufferSize is the default maximum read buffer size.
|
||||||
|
DefaultReadBufferSize = 25 * 1024 * 1024
|
||||||
|
|
||||||
|
// DefaultWriteBufferSize is the default maximum write buffer size.
|
||||||
|
DefaultWriteBufferSize = 10 * 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transport is the common interface to send/receive messages to a target.
|
||||||
|
type Transport interface {
|
||||||
|
Read() (*cdproto.Message, error)
|
||||||
|
Write(*cdproto.Message) error
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conn wraps a gorilla/websocket.Conn connection.
|
||||||
|
type Conn struct {
|
||||||
|
*websocket.Conn
|
||||||
|
dbgf func(string, ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext dials the specified websocket URL using gorilla/websocket.
|
||||||
|
func DialContext(ctx context.Context, urlstr string, opts ...DialOption) (*Conn, error) {
|
||||||
|
d := &websocket.Dialer{
|
||||||
|
ReadBufferSize: DefaultReadBufferSize,
|
||||||
|
WriteBufferSize: DefaultWriteBufferSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect
|
||||||
|
conn, _, err := d.DialContext(ctx, urlstr, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply opts
|
||||||
|
c := &Conn{
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads the next message.
|
||||||
|
func (c *Conn) Read() (*cdproto.Message, error) {
|
||||||
|
// get websocket reader
|
||||||
|
typ, r, err := c.NextReader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if typ != websocket.TextMessage {
|
||||||
|
return nil, ErrInvalidWebsocketMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// when dbgf defined, buffer, log, unmarshal
|
||||||
|
if c.dbgf != nil {
|
||||||
|
// buffer output
|
||||||
|
buf, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.dbgf("<- %s", string(buf))
|
||||||
|
msg := new(cdproto.Message)
|
||||||
|
if err = easyjson.Unmarshal(buf, msg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal direct from reader
|
||||||
|
msg := new(cdproto.Message)
|
||||||
|
if err = easyjson.UnmarshalFromReader(r, msg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes a message.
|
||||||
|
func (c *Conn) Write(msg *cdproto.Message) error {
|
||||||
|
w, err := c.NextWriter(websocket.TextMessage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.dbgf != nil {
|
||||||
|
var buf []byte
|
||||||
|
buf, err = easyjson.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.dbgf("-> %s", string(buf))
|
||||||
|
_, err = w.Write(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// direct marshal
|
||||||
|
_, err = easyjson.MarshalToWriter(msg, w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceIP forces the host component in urlstr to be an IP address.
|
||||||
|
//
|
||||||
|
// Since Chrome 66+, Chrome DevTools Protocol clients connecting to a browser
|
||||||
|
// must send the "Host:" header as either an IP address, or "localhost".
|
||||||
|
func ForceIP(urlstr string) string {
|
||||||
|
if i := strings.Index(urlstr, "://"); i != -1 {
|
||||||
|
scheme := urlstr[:i+3]
|
||||||
|
host, port, path := urlstr[len(scheme)+3:], "", ""
|
||||||
|
if i := strings.Index(host, "/"); i != -1 {
|
||||||
|
host, path = host[:i], host[i:]
|
||||||
|
}
|
||||||
|
if i := strings.Index(host, ":"); i != -1 {
|
||||||
|
host, port = host[:i], host[i:]
|
||||||
|
}
|
||||||
|
if addr, err := net.ResolveIPAddr("ip", host); err == nil {
|
||||||
|
urlstr = scheme + addr.IP.String() + port + path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urlstr
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialOption is a dial option.
|
||||||
|
type DialOption func(*Conn)
|
||||||
|
|
||||||
|
// WithConnDebugf is a dial option to set a protocol logger.
|
||||||
|
func WithConnDebugf(f func(string, ...interface{})) DialOption {
|
||||||
|
return func(c *Conn) {
|
||||||
|
c.dbgf = f
|
||||||
|
}
|
||||||
|
}
|
@ -1,34 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
SRC=$(realpath $(cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/../)
|
|
||||||
|
|
||||||
pushd $SRC &> /dev/null
|
|
||||||
|
|
||||||
gometalinter \
|
|
||||||
--disable=aligncheck \
|
|
||||||
--enable=misspell \
|
|
||||||
--enable=gofmt \
|
|
||||||
--deadline=100s \
|
|
||||||
--cyclo-over=25 \
|
|
||||||
--sort=path \
|
|
||||||
--exclude='\(defer (.+?)\)\) \(errcheck\)$' \
|
|
||||||
--exclude='/easyjson\.go.*(passes|copies) lock' \
|
|
||||||
--exclude='/easyjson\.go.*ineffectual assignment' \
|
|
||||||
--exclude='/easyjson\.go.*unnecessary conversion' \
|
|
||||||
--exclude='/easyjson\.go.*this value of key is never used' \
|
|
||||||
--exclude='/easyjson\.go.*\((gocyclo|golint|goconst|staticcheck)\)$' \
|
|
||||||
--exclude='^cdp/.*Potential hardcoded credentials' \
|
|
||||||
--exclude='^cdp/cdp\.go.*UnmarshalEasyJSON.*\(gocyclo\)$' \
|
|
||||||
--exclude='^cdp/cdputil/cdputil\.go.*UnmarshalMessage.*\(gocyclo\)$' \
|
|
||||||
--exclude='^cmd/chromedp-gen/.*\((gocyclo|interfacer)\)$' \
|
|
||||||
--exclude='^cmd/chromedp-proxy/main\.go.*\(gas\)$' \
|
|
||||||
--exclude='^cmd/chromedp-gen/fixup/fixup\.go.*\(goconst\)$' \
|
|
||||||
--exclude='^cmd/chromedp-gen/internal/enum\.go.*unreachable' \
|
|
||||||
--exclude='^cmd/chromedp-gen/(main|domain-gen)\.go.*\(gas\)$' \
|
|
||||||
--exclude='^examples/[a-z]+/main\.go.*\(errcheck\)$' \
|
|
||||||
--exclude='^kb/gen\.go.*\((gas|vet)\)$' \
|
|
||||||
--exclude='^runner/.*\(gas\)$' \
|
|
||||||
--exclude='^handler\.go.*cmd can be easyjson\.Marshaler' \
|
|
||||||
./...
|
|
||||||
|
|
||||||
popd &> /dev/null
|
|
@ -1,10 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
TMP=$(mktemp -d /tmp/google-chrome.XXXXX)
|
|
||||||
|
|
||||||
google-chrome \
|
|
||||||
--user-data-dir=$TMP \
|
|
||||||
--remote-debugging-port=9222 \
|
|
||||||
--no-first-run \
|
|
||||||
--no-default-browser-check \
|
|
||||||
about:blank
|
|
@ -1,14 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
BASE=$(realpath $(cd -P $GOPATH/src/github.com/chromedp && pwd))
|
|
||||||
|
|
||||||
FILES=$(find $BASE/{chromedp*,goquery,examples} -type f -iname \*.go -not -iname \*.qtpl.go -print0|wc -l --files0-from=-|head -n -1)$'\n'
|
|
||||||
|
|
||||||
AUTOG=$(find $BASE/cdproto/ -type f -iname \*.go -not -iname \*easyjson\* -print0|wc -l --files0-from=-|head -n -1)
|
|
||||||
|
|
||||||
if [ "$1" != "--total" ]; then
|
|
||||||
echo -e "code:\n$FILES\n\ngenerated:\n$AUTOG"
|
|
||||||
else
|
|
||||||
echo "code: $(awk '{s+=$1} END {print s}' <<< "$FILES")"
|
|
||||||
echo "generated: $(awk '{s+=$1} END {print s}' <<< "$AUTOG")"
|
|
||||||
fi
|
|
@ -10,6 +10,9 @@ func (err Error) Error() string {
|
|||||||
|
|
||||||
// Error types.
|
// Error types.
|
||||||
const (
|
const (
|
||||||
|
// ErrInvalidWebsocketMessage is the invalid websocket message.
|
||||||
|
ErrInvalidWebsocketMessage Error = "invalid websocket message"
|
||||||
|
|
||||||
// ErrInvalidDimensions is the invalid dimensions error.
|
// ErrInvalidDimensions is the invalid dimensions error.
|
||||||
ErrInvalidDimensions Error = "invalid dimensions"
|
ErrInvalidDimensions Error = "invalid dimensions"
|
||||||
|
|
||||||
@ -39,4 +42,7 @@ const (
|
|||||||
|
|
||||||
// ErrInvalidHandler is the invalid handler error.
|
// ErrInvalidHandler is the invalid handler error.
|
||||||
ErrInvalidHandler Error = "invalid handler"
|
ErrInvalidHandler Error = "invalid handler"
|
||||||
|
|
||||||
|
// ErrInvalidContext is the invalid context error.
|
||||||
|
ErrInvalidContext Error = "invalid context"
|
||||||
)
|
)
|
||||||
|
6
eval.go
6
eval.go
@ -11,7 +11,7 @@ import (
|
|||||||
// Evaluate is an action to evaluate the Javascript expression, unmarshaling
|
// Evaluate is an action to evaluate the Javascript expression, unmarshaling
|
||||||
// the result of the script evaluation to res.
|
// the result of the script evaluation to res.
|
||||||
//
|
//
|
||||||
// When res is a type other than *[]byte, or **chromedp/cdp/runtime.RemoteObject,
|
// When res is a type other than *[]byte, or **chromedp/cdproto/runtime.RemoteObject,
|
||||||
// then the result of the script evaluation will be returned "by value" (ie,
|
// then the result of the script evaluation will be returned "by value" (ie,
|
||||||
// JSON-encoded), and subsequently an attempt will be made to json.Unmarshal
|
// JSON-encoded), and subsequently an attempt will be made to json.Unmarshal
|
||||||
// the script result to res.
|
// the script result to res.
|
||||||
@ -27,7 +27,7 @@ func Evaluate(expression string, res interface{}, opts ...EvaluateOption) Action
|
|||||||
panic("res cannot be nil")
|
panic("res cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
// set up parameters
|
// set up parameters
|
||||||
p := runtime.Evaluate(expression)
|
p := runtime.Evaluate(expression)
|
||||||
switch res.(type) {
|
switch res.(type) {
|
||||||
@ -42,7 +42,7 @@ func Evaluate(expression string, res interface{}, opts ...EvaluateOption) Action
|
|||||||
}
|
}
|
||||||
|
|
||||||
// evaluate
|
// evaluate
|
||||||
v, exp, err := p.Do(ctxt, h)
|
v, exp, err := p.Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
100
example_test.go
Normal file
100
example_test.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package chromedp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.loafle.net/commons_go/chromedp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleTitle() {
|
||||||
|
ctx, cancel := chromedp.NewContext(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var title string
|
||||||
|
if err := chromedp.Run(ctx,
|
||||||
|
chromedp.Navigate("https://git.loafle.net/commons_go/chromedp/issues"),
|
||||||
|
chromedp.WaitVisible("#start-of-content", chromedp.ByID),
|
||||||
|
chromedp.Title(&title),
|
||||||
|
); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(title)
|
||||||
|
|
||||||
|
// no expected output, to not run this test as part of 'go test'; it's
|
||||||
|
// too slow, requiring internet access.
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleExecAllocator() {
|
||||||
|
dir, err := ioutil.TempDir("", "chromedp-example")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
opts := []chromedp.ExecAllocatorOption{
|
||||||
|
chromedp.NoFirstRun,
|
||||||
|
chromedp.NoDefaultBrowserCheck,
|
||||||
|
chromedp.Headless,
|
||||||
|
chromedp.DisableGPU,
|
||||||
|
chromedp.UserDataDir(dir),
|
||||||
|
}
|
||||||
|
|
||||||
|
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// also set up a custom logger
|
||||||
|
taskCtx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// ensure that the browser process is started
|
||||||
|
if err := chromedp.Run(taskCtx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(dir, "DevToolsActivePort")
|
||||||
|
bs, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
lines := bytes.Split(bs, []byte("\n"))
|
||||||
|
fmt.Printf("DevToolsActivePort has %d lines\n", len(lines))
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// DevToolsActivePort has 2 lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNewContext_manyTabs() {
|
||||||
|
// new browser, first tab
|
||||||
|
ctx1, cancel := chromedp.NewContext(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// ensure the first tab is created
|
||||||
|
if err := chromedp.Run(ctx1); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// same browser, second tab
|
||||||
|
ctx2, _ := chromedp.NewContext(ctx1)
|
||||||
|
|
||||||
|
// ensure the second tab is created
|
||||||
|
if err := chromedp.Run(ctx2); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c1 := chromedp.FromContext(ctx1)
|
||||||
|
c2 := chromedp.FromContext(ctx2)
|
||||||
|
|
||||||
|
fmt.Printf("Same browser: %t\n", c1.Browser == c2.Browser)
|
||||||
|
fmt.Printf("Same tab: %t\n", c1.Target == c2.Target)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Same browser: true
|
||||||
|
// Same tab: false
|
||||||
|
}
|
9
go.mod
9
go.mod
@ -1,9 +1,10 @@
|
|||||||
module github.com/chromedp/chromedp
|
module git.loafle.net/commons_go/chromedp
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2
|
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a
|
||||||
github.com/disintegration/imaging v1.6.0
|
github.com/disintegration/imaging v1.6.0
|
||||||
github.com/gorilla/websocket v1.4.0
|
github.com/gorilla/websocket v1.4.0
|
||||||
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983
|
||||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9 // indirect
|
|
||||||
)
|
)
|
||||||
|
11
go.sum
11
go.sum
@ -1,5 +1,5 @@
|
|||||||
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2 h1:4Ck8YOuS0G3+0xMb80cDSff7QpUolhSc0PGyfagbcdA=
|
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a h1:GZPhzysmNSpFnYVSzixFV/ECNILkkn5HJon7AOUNizg=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
||||||
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
|
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
|
||||||
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
@ -7,10 +7,7 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA
|
|||||||
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls=
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls=
|
||||||
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f h1:B6PQkurxGG1rqEX96oE14gbj8bqvYC5dtks9r5uGmlE=
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 h1:wL11wNW7dhKIcRCHSm4sHKPWz0tt4mwBsVodG7+Xyqg=
|
||||||
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9 h1:+vH8qNweCrORN49012OX3h0oWEXO3p+rRnpAGQinddk=
|
|
||||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
|
657
handler.go
657
handler.go
@ -1,657 +0,0 @@
|
|||||||
package chromedp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
goruntime "runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mailru/easyjson"
|
|
||||||
|
|
||||||
"github.com/chromedp/cdproto"
|
|
||||||
"github.com/chromedp/cdproto/cdp"
|
|
||||||
"github.com/chromedp/cdproto/css"
|
|
||||||
"github.com/chromedp/cdproto/dom"
|
|
||||||
"github.com/chromedp/cdproto/inspector"
|
|
||||||
"github.com/chromedp/cdproto/log"
|
|
||||||
"github.com/chromedp/cdproto/page"
|
|
||||||
"github.com/chromedp/cdproto/runtime"
|
|
||||||
|
|
||||||
"github.com/chromedp/chromedp/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TargetHandler manages a Chrome DevTools Protocol target.
|
|
||||||
type TargetHandler struct {
|
|
||||||
conn client.Transport
|
|
||||||
|
|
||||||
// frames is the set of encountered frames.
|
|
||||||
frames map[cdp.FrameID]*cdp.Frame
|
|
||||||
|
|
||||||
// cur is the current top level frame.
|
|
||||||
cur *cdp.Frame
|
|
||||||
|
|
||||||
// qcmd is the outgoing message queue.
|
|
||||||
qcmd chan *cdproto.Message
|
|
||||||
|
|
||||||
// qres is the incoming command result queue.
|
|
||||||
qres chan *cdproto.Message
|
|
||||||
|
|
||||||
// qevents is the incoming event queue.
|
|
||||||
qevents chan *cdproto.Message
|
|
||||||
|
|
||||||
// detached is closed when the detached event is received.
|
|
||||||
detached chan *inspector.EventDetached
|
|
||||||
|
|
||||||
pageWaitGroup, domWaitGroup *sync.WaitGroup
|
|
||||||
|
|
||||||
// last is the last sent message identifier.
|
|
||||||
last int64
|
|
||||||
lastm sync.Mutex
|
|
||||||
|
|
||||||
// res is the id->result channel map.
|
|
||||||
res map[int64]chan *cdproto.Message
|
|
||||||
resrw sync.RWMutex
|
|
||||||
|
|
||||||
// logging funcs
|
|
||||||
logf, debugf, errf func(string, ...interface{})
|
|
||||||
|
|
||||||
sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTargetHandler creates a new handler for the specified client target.
|
|
||||||
func NewTargetHandler(t client.Target, logf, debugf, errf func(string, ...interface{})) (*TargetHandler, error) {
|
|
||||||
conn, err := client.Dial(t.GetWebsocketURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &TargetHandler{
|
|
||||||
conn: conn,
|
|
||||||
logf: logf,
|
|
||||||
debugf: debugf,
|
|
||||||
errf: errf,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run starts the processing of commands and events of the client target
|
|
||||||
// provided to NewTargetHandler.
|
|
||||||
//
|
|
||||||
// Callers can stop Run by closing the passed context.
|
|
||||||
func (h *TargetHandler) Run(ctxt context.Context) error {
|
|
||||||
// reset
|
|
||||||
h.Lock()
|
|
||||||
h.frames = make(map[cdp.FrameID]*cdp.Frame)
|
|
||||||
h.qcmd = make(chan *cdproto.Message)
|
|
||||||
h.qres = make(chan *cdproto.Message)
|
|
||||||
h.qevents = make(chan *cdproto.Message)
|
|
||||||
h.res = make(map[int64]chan *cdproto.Message)
|
|
||||||
h.detached = make(chan *inspector.EventDetached, 1)
|
|
||||||
h.pageWaitGroup = new(sync.WaitGroup)
|
|
||||||
h.domWaitGroup = new(sync.WaitGroup)
|
|
||||||
h.Unlock()
|
|
||||||
|
|
||||||
// run
|
|
||||||
go h.run(ctxt)
|
|
||||||
|
|
||||||
// enable domains
|
|
||||||
for _, a := range []Action{
|
|
||||||
log.Enable(),
|
|
||||||
runtime.Enable(),
|
|
||||||
//network.Enable(),
|
|
||||||
inspector.Enable(),
|
|
||||||
page.Enable(),
|
|
||||||
dom.Enable(),
|
|
||||||
css.Enable(),
|
|
||||||
} {
|
|
||||||
if err := a.Do(ctxt, h); err != nil {
|
|
||||||
return fmt.Errorf("unable to execute %s: %v", reflect.TypeOf(a), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Lock()
|
|
||||||
|
|
||||||
// get page resources
|
|
||||||
tree, err := page.GetResourceTree().Do(ctxt, h)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get resource tree: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.frames[tree.Frame.ID] = tree.Frame
|
|
||||||
h.cur = tree.Frame
|
|
||||||
|
|
||||||
for _, c := range tree.ChildFrames {
|
|
||||||
h.frames[c.Frame.ID] = c.Frame
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Unlock()
|
|
||||||
|
|
||||||
h.documentUpdated(ctxt)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// run handles the actual message processing to / from the web socket connection.
|
|
||||||
func (h *TargetHandler) run(ctxt context.Context) {
|
|
||||||
defer h.conn.Close()
|
|
||||||
|
|
||||||
// add cancel to context
|
|
||||||
ctxt, cancel := context.WithCancel(ctxt)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
default:
|
|
||||||
msg, err := h.read()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case msg.Method != "":
|
|
||||||
h.qevents <- msg
|
|
||||||
|
|
||||||
case msg.ID != 0:
|
|
||||||
h.qres <- msg
|
|
||||||
|
|
||||||
default:
|
|
||||||
h.errf("ignoring malformed incoming message (missing id or method): %#v", msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-h.detached:
|
|
||||||
// FIXME: should log when detached, and reason
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// process queues
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case ev := <-h.qevents:
|
|
||||||
err := h.processEvent(ctxt, ev)
|
|
||||||
if err != nil {
|
|
||||||
h.errf("could not process event %s: %v", ev.Method, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case res := <-h.qres:
|
|
||||||
err := h.processResult(res)
|
|
||||||
if err != nil {
|
|
||||||
h.errf("could not process result for message %d: %v", res.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case cmd := <-h.qcmd:
|
|
||||||
err := h.processCommand(cmd)
|
|
||||||
if err != nil {
|
|
||||||
h.errf("could not process command message %d: %v", cmd.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// read reads a message from the client connection.
|
|
||||||
func (h *TargetHandler) read() (*cdproto.Message, error) {
|
|
||||||
// read
|
|
||||||
buf, err := h.conn.Read()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.debugf("-> %s", string(buf))
|
|
||||||
|
|
||||||
// unmarshal
|
|
||||||
msg := new(cdproto.Message)
|
|
||||||
err = json.Unmarshal(buf, msg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processEvent processes an incoming event.
|
|
||||||
func (h *TargetHandler) processEvent(ctxt context.Context, msg *cdproto.Message) error {
|
|
||||||
if msg == nil {
|
|
||||||
return ErrChannelClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
// unmarshal
|
|
||||||
ev, err := cdproto.UnmarshalMessage(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch e := ev.(type) {
|
|
||||||
case *inspector.EventDetached:
|
|
||||||
h.Lock()
|
|
||||||
defer h.Unlock()
|
|
||||||
h.detached <- e
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case *dom.EventDocumentUpdated:
|
|
||||||
h.domWaitGroup.Wait()
|
|
||||||
go h.documentUpdated(ctxt)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
d := msg.Method.Domain()
|
|
||||||
if d != "Page" && d != "DOM" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch d {
|
|
||||||
case "Page":
|
|
||||||
h.pageWaitGroup.Add(1)
|
|
||||||
go h.pageEvent(ctxt, ev)
|
|
||||||
|
|
||||||
case "DOM":
|
|
||||||
h.domWaitGroup.Add(1)
|
|
||||||
go h.domEvent(ctxt, ev)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// documentUpdated handles the document updated event, retrieving the document
|
|
||||||
// root for the root frame.
|
|
||||||
func (h *TargetHandler) documentUpdated(ctxt context.Context) {
|
|
||||||
f, err := h.WaitFrame(ctxt, cdp.EmptyFrameID)
|
|
||||||
if err != nil {
|
|
||||||
h.errf("could not get current frame: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Lock()
|
|
||||||
defer f.Unlock()
|
|
||||||
|
|
||||||
// invalidate nodes
|
|
||||||
if f.Root != nil {
|
|
||||||
close(f.Root.Invalidated)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Nodes = make(map[cdp.NodeID]*cdp.Node)
|
|
||||||
f.Root, err = dom.GetDocument().WithPierce(true).Do(ctxt, h)
|
|
||||||
if err != nil {
|
|
||||||
h.errf("could not retrieve document root for %s: %v", f.ID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f.Root.Invalidated = make(chan struct{})
|
|
||||||
walk(f.Nodes, f.Root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// processResult processes an incoming command result.
|
|
||||||
func (h *TargetHandler) processResult(msg *cdproto.Message) error {
|
|
||||||
h.resrw.RLock()
|
|
||||||
defer h.resrw.RUnlock()
|
|
||||||
|
|
||||||
ch, ok := h.res[msg.ID]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("id %d not present in res map", msg.ID)
|
|
||||||
}
|
|
||||||
defer close(ch)
|
|
||||||
|
|
||||||
ch <- msg
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processCommand writes a command to the client connection.
|
|
||||||
func (h *TargetHandler) processCommand(cmd *cdproto.Message) error {
|
|
||||||
// marshal
|
|
||||||
buf, err := json.Marshal(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.debugf("<- %s", string(buf))
|
|
||||||
|
|
||||||
return h.conn.Write(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// emptyObj is an empty JSON object message.
|
|
||||||
var emptyObj = easyjson.RawMessage([]byte(`{}`))
|
|
||||||
|
|
||||||
// Execute executes commandType against the endpoint passed to Run, using the
|
|
||||||
// provided context and params, decoding the result of the command to res.
|
|
||||||
func (h *TargetHandler) Execute(ctxt context.Context, methodType string, params json.Marshaler, res json.Unmarshaler) error {
|
|
||||||
var paramsBuf easyjson.RawMessage
|
|
||||||
if params == nil {
|
|
||||||
paramsBuf = emptyObj
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
paramsBuf, err = json.Marshal(params)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
id := h.next()
|
|
||||||
|
|
||||||
// save channel
|
|
||||||
ch := make(chan *cdproto.Message, 1)
|
|
||||||
h.resrw.Lock()
|
|
||||||
h.res[id] = ch
|
|
||||||
h.resrw.Unlock()
|
|
||||||
|
|
||||||
// queue message
|
|
||||||
h.qcmd <- &cdproto.Message{
|
|
||||||
ID: id,
|
|
||||||
Method: cdproto.MethodType(methodType),
|
|
||||||
Params: paramsBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
errch := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
defer close(errch)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case msg := <-ch:
|
|
||||||
switch {
|
|
||||||
case msg == nil:
|
|
||||||
errch <- ErrChannelClosed
|
|
||||||
|
|
||||||
case msg.Error != nil:
|
|
||||||
errch <- msg.Error
|
|
||||||
|
|
||||||
case res != nil:
|
|
||||||
errch <- json.Unmarshal(msg.Result, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
errch <- ctxt.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
h.resrw.Lock()
|
|
||||||
defer h.resrw.Unlock()
|
|
||||||
|
|
||||||
delete(h.res, id)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return <-errch
|
|
||||||
}
|
|
||||||
|
|
||||||
// next returns the next message id.
|
|
||||||
func (h *TargetHandler) next() int64 {
|
|
||||||
h.lastm.Lock()
|
|
||||||
defer h.lastm.Unlock()
|
|
||||||
h.last++
|
|
||||||
return h.last
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoot returns the current top level frame's root document node.
|
|
||||||
func (h *TargetHandler) GetRoot(ctxt context.Context) (*cdp.Node, error) {
|
|
||||||
var root *cdp.Node
|
|
||||||
|
|
||||||
for {
|
|
||||||
var cur *cdp.Frame
|
|
||||||
select {
|
|
||||||
default:
|
|
||||||
h.RLock()
|
|
||||||
cur = h.cur
|
|
||||||
if cur != nil {
|
|
||||||
cur.RLock()
|
|
||||||
root = cur.Root
|
|
||||||
cur.RUnlock()
|
|
||||||
}
|
|
||||||
h.RUnlock()
|
|
||||||
|
|
||||||
if cur != nil && root != nil {
|
|
||||||
return root, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(DefaultCheckDuration)
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
return nil, ctxt.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetActive sets the currently active frame after a successful navigation.
|
|
||||||
func (h *TargetHandler) SetActive(ctxt context.Context, id cdp.FrameID) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// get frame
|
|
||||||
f, err := h.WaitFrame(ctxt, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Lock()
|
|
||||||
defer h.Unlock()
|
|
||||||
|
|
||||||
h.cur = f
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitFrame waits for a frame to be loaded using the provided context.
|
|
||||||
func (h *TargetHandler) WaitFrame(ctxt context.Context, id cdp.FrameID) (*cdp.Frame, error) {
|
|
||||||
// TODO: fix this
|
|
||||||
timeout := time.After(10 * time.Second)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
default:
|
|
||||||
var f *cdp.Frame
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
h.RLock()
|
|
||||||
if id == cdp.EmptyFrameID {
|
|
||||||
f, ok = h.cur, h.cur != nil
|
|
||||||
} else {
|
|
||||||
f, ok = h.frames[id]
|
|
||||||
}
|
|
||||||
h.RUnlock()
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(DefaultCheckDuration)
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
return nil, ctxt.Err()
|
|
||||||
|
|
||||||
case <-timeout:
|
|
||||||
return nil, fmt.Errorf("timeout waiting for frame `%s`", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitNode waits for a node to be loaded using the provided context.
|
|
||||||
func (h *TargetHandler) WaitNode(ctxt context.Context, f *cdp.Frame, id cdp.NodeID) (*cdp.Node, error) {
|
|
||||||
// TODO: fix this
|
|
||||||
timeout := time.After(10 * time.Second)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
default:
|
|
||||||
var n *cdp.Node
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
f.RLock()
|
|
||||||
n, ok = f.Nodes[id]
|
|
||||||
f.RUnlock()
|
|
||||||
|
|
||||||
if n != nil && ok {
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(DefaultCheckDuration)
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
return nil, ctxt.Err()
|
|
||||||
|
|
||||||
case <-timeout:
|
|
||||||
return nil, fmt.Errorf("timeout waiting for node `%d`", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pageEvent handles incoming page events.
|
|
||||||
func (h *TargetHandler) pageEvent(ctxt context.Context, ev interface{}) {
|
|
||||||
defer h.pageWaitGroup.Done()
|
|
||||||
|
|
||||||
var id cdp.FrameID
|
|
||||||
var op frameOp
|
|
||||||
|
|
||||||
switch e := ev.(type) {
|
|
||||||
case *page.EventFrameNavigated:
|
|
||||||
h.Lock()
|
|
||||||
h.frames[e.Frame.ID] = e.Frame
|
|
||||||
if h.cur != nil && h.cur.ID == e.Frame.ID {
|
|
||||||
h.cur = e.Frame
|
|
||||||
}
|
|
||||||
h.Unlock()
|
|
||||||
return
|
|
||||||
|
|
||||||
case *page.EventFrameAttached:
|
|
||||||
id, op = e.FrameID, frameAttached(e.ParentFrameID)
|
|
||||||
|
|
||||||
case *page.EventFrameDetached:
|
|
||||||
id, op = e.FrameID, frameDetached
|
|
||||||
|
|
||||||
case *page.EventFrameStartedLoading:
|
|
||||||
id, op = e.FrameID, frameStartedLoading
|
|
||||||
|
|
||||||
case *page.EventFrameStoppedLoading:
|
|
||||||
id, op = e.FrameID, frameStoppedLoading
|
|
||||||
|
|
||||||
case *page.EventFrameScheduledNavigation:
|
|
||||||
id, op = e.FrameID, frameScheduledNavigation
|
|
||||||
|
|
||||||
case *page.EventFrameClearedScheduledNavigation:
|
|
||||||
id, op = e.FrameID, frameClearedScheduledNavigation
|
|
||||||
|
|
||||||
// ignored events
|
|
||||||
case *page.EventDomContentEventFired:
|
|
||||||
return
|
|
||||||
case *page.EventLoadEventFired:
|
|
||||||
return
|
|
||||||
case *page.EventFrameResized:
|
|
||||||
return
|
|
||||||
case *page.EventLifecycleEvent:
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
h.errf("unhandled page event %s", reflect.TypeOf(ev))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := h.WaitFrame(ctxt, id)
|
|
||||||
if err != nil {
|
|
||||||
h.errf("could not get frame %s: %v", id, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Lock()
|
|
||||||
defer h.Unlock()
|
|
||||||
|
|
||||||
f.Lock()
|
|
||||||
defer f.Unlock()
|
|
||||||
|
|
||||||
op(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// domEvent handles incoming DOM events.
|
|
||||||
func (h *TargetHandler) domEvent(ctxt context.Context, ev interface{}) {
|
|
||||||
defer h.domWaitGroup.Done()
|
|
||||||
|
|
||||||
// wait current frame
|
|
||||||
f, err := h.WaitFrame(ctxt, cdp.EmptyFrameID)
|
|
||||||
if err != nil {
|
|
||||||
h.errf("could not process DOM event %s: %v", reflect.TypeOf(ev), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var id cdp.NodeID
|
|
||||||
var op nodeOp
|
|
||||||
|
|
||||||
switch e := ev.(type) {
|
|
||||||
case *dom.EventSetChildNodes:
|
|
||||||
id, op = e.ParentID, setChildNodes(f.Nodes, e.Nodes)
|
|
||||||
|
|
||||||
case *dom.EventAttributeModified:
|
|
||||||
id, op = e.NodeID, attributeModified(e.Name, e.Value)
|
|
||||||
|
|
||||||
case *dom.EventAttributeRemoved:
|
|
||||||
id, op = e.NodeID, attributeRemoved(e.Name)
|
|
||||||
|
|
||||||
case *dom.EventInlineStyleInvalidated:
|
|
||||||
if len(e.NodeIds) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, op = e.NodeIds[0], inlineStyleInvalidated(e.NodeIds[1:])
|
|
||||||
|
|
||||||
case *dom.EventCharacterDataModified:
|
|
||||||
id, op = e.NodeID, characterDataModified(e.CharacterData)
|
|
||||||
|
|
||||||
case *dom.EventChildNodeCountUpdated:
|
|
||||||
id, op = e.NodeID, childNodeCountUpdated(e.ChildNodeCount)
|
|
||||||
|
|
||||||
case *dom.EventChildNodeInserted:
|
|
||||||
if e.PreviousNodeID != cdp.EmptyNodeID {
|
|
||||||
_, err = h.WaitNode(ctxt, f, e.PreviousNodeID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
id, op = e.ParentNodeID, childNodeInserted(f.Nodes, e.PreviousNodeID, e.Node)
|
|
||||||
|
|
||||||
case *dom.EventChildNodeRemoved:
|
|
||||||
id, op = e.ParentNodeID, childNodeRemoved(f.Nodes, e.NodeID)
|
|
||||||
|
|
||||||
case *dom.EventShadowRootPushed:
|
|
||||||
id, op = e.HostID, shadowRootPushed(f.Nodes, e.Root)
|
|
||||||
|
|
||||||
case *dom.EventShadowRootPopped:
|
|
||||||
id, op = e.HostID, shadowRootPopped(f.Nodes, e.RootID)
|
|
||||||
|
|
||||||
case *dom.EventPseudoElementAdded:
|
|
||||||
id, op = e.ParentID, pseudoElementAdded(f.Nodes, e.PseudoElement)
|
|
||||||
|
|
||||||
case *dom.EventPseudoElementRemoved:
|
|
||||||
id, op = e.ParentID, pseudoElementRemoved(f.Nodes, e.PseudoElementID)
|
|
||||||
|
|
||||||
case *dom.EventDistributedNodesUpdated:
|
|
||||||
id, op = e.InsertionPointID, distributedNodesUpdated(e.DistributedNodes)
|
|
||||||
|
|
||||||
default:
|
|
||||||
h.errf("unhandled node event %s", reflect.TypeOf(ev))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieve node
|
|
||||||
n, err := h.WaitNode(ctxt, f, id)
|
|
||||||
if err != nil {
|
|
||||||
s := strings.TrimSuffix(goruntime.FuncForPC(reflect.ValueOf(op).Pointer()).Name(), ".func1")
|
|
||||||
i := strings.LastIndex(s, ".")
|
|
||||||
if i != -1 {
|
|
||||||
s = s[i+1:]
|
|
||||||
}
|
|
||||||
h.errf("could not perform (%s) operation on node %d (wait node): %v", s, id, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Lock()
|
|
||||||
defer h.Unlock()
|
|
||||||
|
|
||||||
f.Lock()
|
|
||||||
defer f.Unlock()
|
|
||||||
|
|
||||||
op(n)
|
|
||||||
}
|
|
36
input.go
36
input.go
@ -3,13 +3,12 @@ package chromedp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/cdp"
|
"github.com/chromedp/cdproto/cdp"
|
||||||
"github.com/chromedp/cdproto/dom"
|
"github.com/chromedp/cdproto/dom"
|
||||||
"github.com/chromedp/cdproto/input"
|
"github.com/chromedp/cdproto/input"
|
||||||
|
|
||||||
"github.com/chromedp/chromedp/kb"
|
"git.loafle.net/commons_go/chromedp/kb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MouseAction is a mouse action.
|
// MouseAction is a mouse action.
|
||||||
@ -27,7 +26,7 @@ func MouseAction(typ input.MouseType, x, y int64, opts ...MouseOption) Action {
|
|||||||
// MouseClickXY sends a left mouse button click (ie, mousePressed and
|
// MouseClickXY sends a left mouse button click (ie, mousePressed and
|
||||||
// mouseReleased event) at the X, Y location.
|
// mouseReleased event) at the X, Y location.
|
||||||
func MouseClickXY(x, y int64, opts ...MouseOption) Action {
|
func MouseClickXY(x, y int64, opts ...MouseOption) Action {
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
me := &input.DispatchMouseEventParams{
|
me := &input.DispatchMouseEventParams{
|
||||||
Type: input.MousePressed,
|
Type: input.MousePressed,
|
||||||
X: float64(x),
|
X: float64(x),
|
||||||
@ -41,13 +40,12 @@ func MouseClickXY(x, y int64, opts ...MouseOption) Action {
|
|||||||
me = o(me)
|
me = o(me)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := me.Do(ctxt, h)
|
if err := me.Do(ctx, h); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
me.Type = input.MouseReleased
|
me.Type = input.MouseReleased
|
||||||
return me.Do(ctxt, h)
|
return me.Do(ctx, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,16 +55,14 @@ func MouseClickXY(x, y int64, opts ...MouseOption) Action {
|
|||||||
// Note that the window will be scrolled if the node is not within the window's
|
// Note that the window will be scrolled if the node is not within the window's
|
||||||
// viewport.
|
// viewport.
|
||||||
func MouseClickNode(n *cdp.Node, opts ...MouseOption) Action {
|
func MouseClickNode(n *cdp.Node, opts ...MouseOption) Action {
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
var err error
|
|
||||||
|
|
||||||
var pos []int
|
var pos []int
|
||||||
err = EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, n.FullXPath()), &pos).Do(ctxt, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, n.FullXPath()), &pos).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
box, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
box, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -84,7 +80,7 @@ func MouseClickNode(n *cdp.Node, opts ...MouseOption) Action {
|
|||||||
x /= int64(c / 2)
|
x /= int64(c / 2)
|
||||||
y /= int64(c / 2)
|
y /= int64(c / 2)
|
||||||
|
|
||||||
return MouseClickXY(x, y, opts...).Do(ctxt, h)
|
return MouseClickXY(x, y, opts...).Do(ctx, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,19 +149,13 @@ func ClickCount(n int) MouseOption {
|
|||||||
// Please see the chromedp/kb package for implementation details and the list
|
// Please see the chromedp/kb package for implementation details and the list
|
||||||
// of well-known keys.
|
// of well-known keys.
|
||||||
func KeyAction(keys string, opts ...KeyOption) Action {
|
func KeyAction(keys string, opts ...KeyOption) Action {
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
var err error
|
|
||||||
|
|
||||||
for _, r := range keys {
|
for _, r := range keys {
|
||||||
for _, k := range kb.Encode(r) {
|
for _, k := range kb.Encode(r) {
|
||||||
err = k.Do(ctxt, h)
|
if err := k.Do(ctx, h); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move to context
|
|
||||||
time.Sleep(5 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -174,13 +164,13 @@ func KeyAction(keys string, opts ...KeyOption) Action {
|
|||||||
|
|
||||||
// KeyActionNode dispatches a key event on a node.
|
// KeyActionNode dispatches a key event on a node.
|
||||||
func KeyActionNode(n *cdp.Node, keys string, opts ...KeyOption) Action {
|
func KeyActionNode(n *cdp.Node, keys string, opts ...KeyOption) Action {
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
err := dom.Focus().WithNodeID(n.NodeID).Do(ctxt, h)
|
err := dom.Focus().WithNodeID(n.NodeID).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyAction(keys, opts...).Do(ctxt, h)
|
return KeyAction(keys, opts...).Do(ctx, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
146
input_test.go
146
input_test.go
@ -4,35 +4,28 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/cdp"
|
"github.com/chromedp/cdproto/cdp"
|
||||||
"github.com/chromedp/cdproto/input"
|
"github.com/chromedp/cdproto/input"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// inViewportJS is a javascript snippet that will get the specified node
|
||||||
// inViewportJS is a javascript snippet that will get the specified node
|
// position relative to the viewport and returns true if the specified node
|
||||||
// position relative to the viewport and returns true if the specified node
|
// is within the window's viewport.
|
||||||
// is within the window's viewport.
|
const inViewportJS = `(function(a) {
|
||||||
inViewportJS = `(function(a) {
|
|
||||||
var r = a[0].getBoundingClientRect();
|
var r = a[0].getBoundingClientRect();
|
||||||
return r.top >= 0 && r.left >= 0 && r.bottom <= window.innerHeight && r.right <= window.innerWidth;
|
return r.top >= 0 && r.left >= 0 && r.bottom <= window.innerHeight && r.right <= window.innerWidth;
|
||||||
})($x('%s'))`
|
})($x('%s'))`
|
||||||
)
|
|
||||||
|
|
||||||
func TestMouseClickXY(t *testing.T) {
|
func TestMouseClickXY(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "input.html")
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
c := testAllocate(t, "input.html")
|
if err := Run(ctx, WaitVisible(`#input1`, ByID)); err != nil {
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Sleep(100*time.Millisecond))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
x, y int64
|
x, y int64
|
||||||
}{
|
}{
|
||||||
@ -43,18 +36,14 @@ func TestMouseClickXY(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
err = c.Run(defaultContext, MouseClickXY(test.x, test.y))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var xstr, ystr string
|
var xstr, ystr string
|
||||||
err = c.Run(defaultContext, Value("#input1", &xstr, ByID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
MouseClickXY(test.x, test.y),
|
||||||
|
Value("#input1", &xstr, ByID),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
x, err := strconv.ParseInt(xstr, 10, 64)
|
x, err := strconv.ParseInt(xstr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
@ -62,11 +51,10 @@ func TestMouseClickXY(t *testing.T) {
|
|||||||
if x != test.x {
|
if x != test.x {
|
||||||
t.Fatalf("test %d expected x to be: %d, got: %d", i, test.x, x)
|
t.Fatalf("test %d expected x to be: %d, got: %d", i, test.x, x)
|
||||||
}
|
}
|
||||||
|
if err := Run(ctx, Value("#input2", &ystr, ByID)); err != nil {
|
||||||
err = c.Run(defaultContext, Value("#input2", &ystr, ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
y, err := strconv.ParseInt(ystr, 10, 64)
|
y, err := strconv.ParseInt(ystr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
@ -88,40 +76,34 @@ func TestMouseClickNode(t *testing.T) {
|
|||||||
{"button2", "foo", ButtonType(input.ButtonNone), ByID},
|
{"button2", "foo", ButtonType(input.ButtonNone), ByID},
|
||||||
{"button2", "bar", ButtonType(input.ButtonLeft), ByID},
|
{"button2", "bar", ButtonType(input.ButtonLeft), ByID},
|
||||||
{"button2", "bar-middle", ButtonType(input.ButtonMiddle), ByID},
|
{"button2", "bar-middle", ButtonType(input.ButtonMiddle), ByID},
|
||||||
|
{"input3", "foo", ButtonModifiers(input.ModifierNone), ByID},
|
||||||
{"input3", "bar-right", ButtonType(input.ButtonRight), ByID},
|
{"input3", "bar-right", ButtonType(input.ButtonRight), ByID},
|
||||||
{"input3", "bar-right", ButtonModifiers(input.ModifierNone), ByID},
|
|
||||||
{"input3", "bar-right", Button("right"), ByID},
|
{"input3", "bar-right", Button("right"), ByID},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "input.html")
|
ctx, cancel := testAllocate(t, "input.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var err error
|
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if len(nodes) != 1 {
|
if len(nodes) != 1 {
|
||||||
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, MouseClickNode(nodes[0], test.opt))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value("#input3", &value, ByID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
MouseClickNode(nodes[0], test.opt),
|
||||||
|
Value("#input3", &value, ByID),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != test.exp {
|
if value != test.exp {
|
||||||
t.Fatalf("expected to have value %s, got: %s", test.exp, value)
|
t.Fatalf("expected to have value %s, got: %s", test.exp, value)
|
||||||
}
|
}
|
||||||
@ -143,45 +125,42 @@ func TestMouseClickOffscreenNode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "input.html")
|
ctx, cancel := testAllocate(t, "input.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var err error
|
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(nodes) != 1 {
|
if len(nodes) != 1 {
|
||||||
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
err = c.Run(defaultContext, EvaluateAsDevTools(fmt.Sprintf(inViewportJS, nodes[0].FullXPath()), &ok))
|
if err := Run(ctx, EvaluateAsDevTools(fmt.Sprintf(inViewportJS, nodes[0].FullXPath()), &ok)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
t.Fatal("expected node to be offscreen")
|
t.Fatal("expected node to be offscreen")
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := test.exp; i > 0; i-- {
|
for i := test.exp; i > 0; i-- {
|
||||||
err = c.Run(defaultContext, MouseClickNode(nodes[0]))
|
if err := Run(ctx, MouseClickNode(nodes[0])); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
var value int
|
var value int
|
||||||
err = c.Run(defaultContext, Evaluate("window.document.test_i", &value))
|
if err := Run(ctx, Evaluate("window.document.test_i", &value)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != test.exp {
|
if value != test.exp {
|
||||||
t.Fatalf("expected to have value %d, got: %d", test.exp, value)
|
t.Fatalf("expected to have value %d, got: %d", test.exp, value)
|
||||||
}
|
}
|
||||||
@ -205,37 +184,33 @@ func TestKeyAction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "input.html")
|
ctx, cancel := testAllocate(t, "input.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var err error
|
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(nodes) != 1 {
|
if len(nodes) != 1 {
|
||||||
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
||||||
}
|
}
|
||||||
|
if err := Run(ctx,
|
||||||
err = c.Run(defaultContext, Focus(test.sel, test.by))
|
Focus(test.sel, test.by),
|
||||||
if err != nil {
|
KeyAction(test.exp),
|
||||||
t.Fatalf("got error: %v", err)
|
); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, KeyAction(test.exp))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != test.exp {
|
if value != test.exp {
|
||||||
t.Fatalf("expected to have value %s, got: %s", test.exp, value)
|
t.Fatalf("expected to have value %s, got: %s", test.exp, value)
|
||||||
}
|
}
|
||||||
@ -259,32 +234,29 @@ func TestKeyActionNode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "input.html")
|
ctx, cancel := testAllocate(t, "input.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var err error
|
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(nodes) != 1 {
|
if len(nodes) != 1 {
|
||||||
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, KeyActionNode(nodes[0], test.exp))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
KeyActionNode(nodes[0], test.exp),
|
||||||
|
Value(test.sel, &value, test.by),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != test.exp {
|
if value != test.exp {
|
||||||
t.Fatalf("expected to have value %s, got: %s", test.exp, value)
|
t.Fatalf("expected to have value %s, got: %s", test.exp, value)
|
||||||
}
|
}
|
||||||
|
24
kb/gen.go
24
kb/gen.go
@ -16,7 +16,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/chromedp/chromedp/kb"
|
"git.loafle.net/commons_go/chromedp/kb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -72,8 +72,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run() error {
|
func run() error {
|
||||||
var err error
|
|
||||||
|
|
||||||
// special characters
|
// special characters
|
||||||
keys := map[rune]kb.Key{
|
keys := map[rune]kb.Key{
|
||||||
'\b': {"Backspace", "Backspace", "", "", int64('\b'), int64('\b'), false, false},
|
'\b': {"Backspace", "Backspace", "", "", int64('\b'), int64('\b'), false, false},
|
||||||
@ -82,8 +80,7 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load keys
|
// load keys
|
||||||
err = loadKeys(keys)
|
if err := loadKeys(keys); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,24 +91,19 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// output
|
// output
|
||||||
err = ioutil.WriteFile(
|
if err := ioutil.WriteFile(*flagOut,
|
||||||
*flagOut,
|
|
||||||
[]byte(fmt.Sprintf(hdr, *flagPkg, string(constBuf), string(mapBuf))),
|
[]byte(fmt.Sprintf(hdr, *flagPkg, string(constBuf), string(mapBuf))),
|
||||||
0644,
|
0644); err != nil {
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// format
|
// format
|
||||||
err = exec.Command("goimports", "-w", *flagOut).Run()
|
if err := exec.Command("goimports", "-w", *flagOut).Run(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// format
|
// format
|
||||||
err = exec.Command("gofmt", "-s", "-w", *flagOut).Run()
|
if err := exec.Command("gofmt", "-s", "-w", *flagOut).Run(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,8 +112,6 @@ func run() error {
|
|||||||
|
|
||||||
// loadKeys loads the dom key definitions from the chromium source tree.
|
// loadKeys loads the dom key definitions from the chromium source tree.
|
||||||
func loadKeys(keys map[rune]kb.Key) error {
|
func loadKeys(keys map[rune]kb.Key) error {
|
||||||
var err error
|
|
||||||
|
|
||||||
// load key converter data
|
// load key converter data
|
||||||
keycodeConverterMap, err := loadKeycodeConverterData()
|
keycodeConverterMap, err := loadKeycodeConverterData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -444,8 +434,6 @@ var defineRE = regexp.MustCompile(`(?m)^#define\s+(.+?)\s+([0-9A-Fx]+)`)
|
|||||||
// loadPosixWinKeyboardCodes loads the native and windows keyboard scan codes
|
// loadPosixWinKeyboardCodes loads the native and windows keyboard scan codes
|
||||||
// mapped to the DOM key.
|
// mapped to the DOM key.
|
||||||
func loadPosixWinKeyboardCodes() (map[string][]int64, error) {
|
func loadPosixWinKeyboardCodes() (map[string][]int64, error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
lookup := map[string]string{
|
lookup := map[string]string{
|
||||||
// mac alias
|
// mac alias
|
||||||
"VKEY_LWIN": "0x5B",
|
"VKEY_LWIN": "0x5B",
|
||||||
|
37
nav.go
37
nav.go
@ -10,18 +10,9 @@ import (
|
|||||||
|
|
||||||
// Navigate navigates the current frame.
|
// Navigate navigates the current frame.
|
||||||
func Navigate(urlstr string) Action {
|
func Navigate(urlstr string) Action {
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
th, ok := h.(*TargetHandler)
|
_, _, _, err := page.Navigate(urlstr).Do(ctx, h)
|
||||||
if !ok {
|
|
||||||
return ErrInvalidHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
frameID, _, _, err := page.Navigate(urlstr).Do(ctxt, th)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
return th.SetActive(ctxt, frameID)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,9 +23,9 @@ func NavigationEntries(currentIndex *int64, entries *[]*page.NavigationEntry) Ac
|
|||||||
panic("currentIndex and entries cannot be nil")
|
panic("currentIndex and entries cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
var err error
|
var err error
|
||||||
*currentIndex, *entries, err = page.GetNavigationHistory().Do(ctxt, h)
|
*currentIndex, *entries, err = page.GetNavigationHistory().Do(ctx, h)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -47,8 +38,8 @@ func NavigateToHistoryEntry(entryID int64) Action {
|
|||||||
|
|
||||||
// NavigateBack navigates the current frame backwards in its history.
|
// NavigateBack navigates the current frame backwards in its history.
|
||||||
func NavigateBack() Action {
|
func NavigateBack() Action {
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
cur, entries, err := page.GetNavigationHistory().Do(ctxt, h)
|
cur, entries, err := page.GetNavigationHistory().Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -57,14 +48,14 @@ func NavigateBack() Action {
|
|||||||
return errors.New("invalid navigation entry")
|
return errors.New("invalid navigation entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.NavigateToHistoryEntry(entries[cur-1].ID).Do(ctxt, h)
|
return page.NavigateToHistoryEntry(entries[cur-1].ID).Do(ctx, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NavigateForward navigates the current frame forwards in its history.
|
// NavigateForward navigates the current frame forwards in its history.
|
||||||
func NavigateForward() Action {
|
func NavigateForward() Action {
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
cur, entries, err := page.GetNavigationHistory().Do(ctxt, h)
|
cur, entries, err := page.GetNavigationHistory().Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -73,7 +64,7 @@ func NavigateForward() Action {
|
|||||||
return errors.New("invalid navigation entry")
|
return errors.New("invalid navigation entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.NavigateToHistoryEntry(entries[cur+1].ID).Do(ctxt, h)
|
return page.NavigateToHistoryEntry(entries[cur+1].ID).Do(ctx, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,9 +86,9 @@ func CaptureScreenshot(res *[]byte) Action {
|
|||||||
panic("res cannot be nil")
|
panic("res cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
var err error
|
var err error
|
||||||
*res, err = page.CaptureScreenshot().Do(ctxt, h)
|
*res, err = page.CaptureScreenshot().Do(ctx, h)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -108,9 +99,9 @@ func CaptureScreenshot(res *[]byte) Action {
|
|||||||
panic("id cannot be nil")
|
panic("id cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||||
var err error
|
var err error
|
||||||
*id, err = page.AddScriptToEvaluateOnLoad(source).Do(ctxt, h)
|
*id, err = page.AddScriptToEvaluateOnLoad(source).Do(ctx, h)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
423
nav_test.go
423
nav_test.go
@ -1,47 +1,43 @@
|
|||||||
package chromedp
|
package chromedp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/emulation"
|
||||||
"github.com/chromedp/cdproto/page"
|
"github.com/chromedp/cdproto/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNavigate(t *testing.T) {
|
func TestNavigate(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
|
defer cancel()
|
||||||
c := testAllocate(t, "")
|
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
expurl, exptitle := testdataDir+"/image.html", "this is title"
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(expurl))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitVisible(`#icon-brankas`, ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var urlstr string
|
var urlstr string
|
||||||
err = c.Run(defaultContext, Location(&urlstr))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
WaitVisible(`#icon-brankas`, ByID),
|
||||||
|
Location(&urlstr),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(urlstr, expurl) {
|
if !strings.HasSuffix(urlstr, "image.html") {
|
||||||
t.Errorf("expected to be on image.html, at: %s", urlstr)
|
t.Errorf("expected to be on image.html, at: %s", urlstr)
|
||||||
}
|
}
|
||||||
|
|
||||||
var title string
|
var title string
|
||||||
err = c.Run(defaultContext, Title(&title))
|
if err := Run(ctx, Title(&title)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exptitle := "this is title"
|
||||||
if title != exptitle {
|
if title != exptitle {
|
||||||
t.Errorf("expected title to contain google, instead title is: %s", title)
|
t.Errorf("expected title to contain google, instead title is: %s", title)
|
||||||
}
|
}
|
||||||
@ -50,21 +46,19 @@ func TestNavigate(t *testing.T) {
|
|||||||
func TestNavigationEntries(t *testing.T) {
|
func TestNavigationEntries(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "")
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
c := testAllocate(t, "")
|
tests := []struct {
|
||||||
defer c.Release()
|
file, waitID string
|
||||||
|
}{
|
||||||
tests := []string{
|
{"form.html", "#form"},
|
||||||
"form.html",
|
{"image.html", "#icon-brankas"},
|
||||||
"image.html",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var entries []*page.NavigationEntry
|
var entries []*page.NavigationEntry
|
||||||
var index int64
|
var index int64
|
||||||
|
if err := Run(ctx, NavigationEntries(&index, &entries)); err != nil {
|
||||||
err = c.Run(defaultContext, NavigationEntries(&index, &entries))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,24 +70,19 @@ func TestNavigationEntries(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expIdx, expEntries := 1, 2
|
expIdx, expEntries := 1, 2
|
||||||
for i, url := range tests {
|
for i, test := range tests {
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/"+url))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
Navigate(testdataDir+"/"+test.file),
|
||||||
|
WaitVisible(test.waitID, ByID),
|
||||||
|
NavigationEntries(&index, &entries),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, NavigationEntries(&index, &entries))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(entries) != expEntries {
|
if len(entries) != expEntries {
|
||||||
t.Errorf("test %d expected to have %d navigation entry: got %d", i, expEntries, len(entries))
|
t.Errorf("test %d expected to have %d navigation entry: got %d", i, expEntries, len(entries))
|
||||||
}
|
}
|
||||||
if index != int64(i+1) {
|
if want := int64(i + 1); index != want {
|
||||||
t.Errorf("test %d expected navigation index is %d, got: %d", i, i, index)
|
t.Errorf("test %d expected navigation index is %d, got: %d", i, want, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
expIdx++
|
expIdx++
|
||||||
@ -104,42 +93,27 @@ func TestNavigationEntries(t *testing.T) {
|
|||||||
func TestNavigateToHistoryEntry(t *testing.T) {
|
func TestNavigateToHistoryEntry(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
|
defer cancel()
|
||||||
c := testAllocate(t, "")
|
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
var entries []*page.NavigationEntry
|
var entries []*page.NavigationEntry
|
||||||
var index int64
|
var index int64
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/image.html"))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||||
|
NavigationEntries(&index, &entries),
|
||||||
|
|
||||||
|
Navigate(testdataDir+"/form.html"),
|
||||||
|
WaitVisible(`#form`, ByID), // for form.html
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, NavigationEntries(&index, &entries))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, NavigateToHistoryEntry(entries[index].ID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var title string
|
var title string
|
||||||
err = c.Run(defaultContext, Title(&title))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
NavigateToHistoryEntry(entries[index].ID),
|
||||||
|
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||||
|
Title(&title),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if title != entries[index].Title {
|
if title != entries[index].Title {
|
||||||
@ -150,43 +124,24 @@ func TestNavigateToHistoryEntry(t *testing.T) {
|
|||||||
func TestNavigateBack(t *testing.T) {
|
func TestNavigateBack(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
c := testAllocate(t, "")
|
var title, exptitle string
|
||||||
defer c.Release()
|
if err := Run(ctx,
|
||||||
|
WaitVisible(`#form`, ByID), // for form.html
|
||||||
|
Title(&exptitle),
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
Navigate(testdataDir+"/image.html"),
|
||||||
if err != nil {
|
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||||
|
|
||||||
|
NavigateBack(),
|
||||||
|
WaitVisible(`#form`, ByID), // for form.html
|
||||||
|
Title(&title),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var exptitle string
|
|
||||||
err = c.Run(defaultContext, Title(&exptitle))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/image.html"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, NavigateBack())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var title string
|
|
||||||
err = c.Run(defaultContext, Title(&title))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if title != exptitle {
|
if title != exptitle {
|
||||||
t.Errorf("expected title to be %s, instead title is: %s", exptitle, title)
|
t.Errorf("expected title to be %s, instead title is: %s", exptitle, title)
|
||||||
}
|
}
|
||||||
@ -195,50 +150,27 @@ func TestNavigateBack(t *testing.T) {
|
|||||||
func TestNavigateForward(t *testing.T) {
|
func TestNavigateForward(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
c := testAllocate(t, "")
|
var title, exptitle string
|
||||||
defer c.Release()
|
if err := Run(ctx,
|
||||||
|
WaitVisible(`#form`, ByID), // for form.html
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
Navigate(testdataDir+"/image.html"),
|
||||||
if err != nil {
|
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||||
|
Title(&exptitle),
|
||||||
|
|
||||||
|
NavigateBack(),
|
||||||
|
WaitVisible(`#form`, ByID), // for form.html
|
||||||
|
|
||||||
|
NavigateForward(),
|
||||||
|
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||||
|
Title(&title),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/image.html"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var exptitle string
|
|
||||||
err = c.Run(defaultContext, Title(&exptitle))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, NavigateBack())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, NavigateForward())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var title string
|
|
||||||
err = c.Run(defaultContext, Title(&title))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if title != exptitle {
|
if title != exptitle {
|
||||||
t.Errorf("expected title to be %s, instead title is: %s", exptitle, title)
|
t.Errorf("expected title to be %s, instead title is: %s", exptitle, title)
|
||||||
}
|
}
|
||||||
@ -247,18 +179,9 @@ func TestNavigateForward(t *testing.T) {
|
|||||||
func TestStop(t *testing.T) {
|
func TestStop(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
|
defer cancel()
|
||||||
c := testAllocate(t, "")
|
if err := Run(ctx, Stop()); err != nil {
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Stop())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,36 +189,38 @@ func TestStop(t *testing.T) {
|
|||||||
func TestReload(t *testing.T) {
|
func TestReload(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
count := 0
|
||||||
|
// create test server
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
fmt.Fprintf(res, `<html>
|
||||||
|
<head>
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="count%d"></div>
|
||||||
|
</body></html`, count)
|
||||||
|
count++
|
||||||
|
})
|
||||||
|
s := httptest.NewServer(mux)
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
c := testAllocate(t, "")
|
ctx, cancel := testAllocate(t, "")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
var title, exptitle string
|
||||||
if err != nil {
|
if err := Run(ctx,
|
||||||
|
Navigate(s.URL),
|
||||||
|
WaitReady(`#count0`, ByID),
|
||||||
|
Title(&exptitle),
|
||||||
|
|
||||||
|
Reload(),
|
||||||
|
WaitReady(`#count1`, ByID),
|
||||||
|
Title(&title),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var exptitle string
|
|
||||||
err = c.Run(defaultContext, Title(&exptitle))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Reload())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var title string
|
|
||||||
err = c.Run(defaultContext, Title(&title))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if title != exptitle {
|
if title != exptitle {
|
||||||
t.Errorf("expected title to be %s, instead title is: %s", exptitle, title)
|
t.Errorf("expected title to be %s, instead title is: %s", exptitle, title)
|
||||||
}
|
}
|
||||||
@ -304,51 +229,47 @@ func TestReload(t *testing.T) {
|
|||||||
func TestCaptureScreenshot(t *testing.T) {
|
func TestCaptureScreenshot(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
|
defer cancel()
|
||||||
c := testAllocate(t, "")
|
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/image.html"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
|
// set the viewport size, to know what screenshot size to expect
|
||||||
|
width, height := 650, 450
|
||||||
var buf []byte
|
var buf []byte
|
||||||
err = c.Run(defaultContext, CaptureScreenshot(&buf))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
emulation.SetDeviceMetricsOverride(int64(width), int64(height), 1.0, false),
|
||||||
|
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||||
|
CaptureScreenshot(&buf),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(buf) == 0 {
|
config, format, err := image.DecodeConfig(bytes.NewReader(buf))
|
||||||
t.Fatal("failed to capture screenshot")
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if want := "png"; format != want {
|
||||||
|
t.Fatalf("expected format to be %q, got %q", want, format)
|
||||||
|
}
|
||||||
|
if config.Width != width || config.Height != height {
|
||||||
|
t.Fatalf("expected dimensions to be %d*%d, got %d*%d",
|
||||||
|
width, height, config.Width, config.Height)
|
||||||
}
|
}
|
||||||
//TODO: test image
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*func TestAddOnLoadScript(t *testing.T) {
|
/*func TestAddOnLoadScript(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "")
|
||||||
|
defer cancel()
|
||||||
c := testAllocate(t, "")
|
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
var scriptID page.ScriptIdentifier
|
var scriptID page.ScriptIdentifier
|
||||||
err = c.Run(defaultContext, AddOnLoadScript(`window.alert("TEST")`, &scriptID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
AddOnLoadScript(`window.alert("TEST")`, &scriptID),
|
||||||
|
Navigate(testdataDir+"/form.html"),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
if scriptID == "" {
|
if scriptID == "" {
|
||||||
t.Fatal("got empty script ID")
|
t.Fatal("got empty script ID")
|
||||||
}
|
}
|
||||||
@ -358,57 +279,40 @@ func TestCaptureScreenshot(t *testing.T) {
|
|||||||
func TestRemoveOnLoadScript(t *testing.T) {
|
func TestRemoveOnLoadScript(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "")
|
||||||
|
defer cancel()
|
||||||
c := testAllocate(t, "")
|
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
var scriptID page.ScriptIdentifier
|
var scriptID page.ScriptIdentifier
|
||||||
err = c.Run(defaultContext, AddOnLoadScript(`window.alert("TEST")`, &scriptID))
|
if err := Run(ctx, AddOnLoadScript(`window.alert("TEST")`, &scriptID)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if scriptID == "" {
|
if scriptID == "" {
|
||||||
t.Fatal("got empty script ID")
|
t.Fatal("got empty script ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, RemoveOnLoadScript(scriptID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
RemoveOnLoadScript(scriptID),
|
||||||
|
Navigate(testdataDir+"/form.html"),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
func TestLocation(t *testing.T) {
|
func TestLocation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
expurl := testdataDir + "/form.html"
|
defer cancel()
|
||||||
|
|
||||||
c := testAllocate(t, "")
|
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(expurl))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var urlstr string
|
var urlstr string
|
||||||
err = c.Run(defaultContext, Location(&urlstr))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
WaitVisible(`#form`, ByID), // for form.html
|
||||||
|
Location(&urlstr),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if urlstr != expurl {
|
if !strings.HasSuffix(urlstr, "form.html") {
|
||||||
t.Fatalf("expected to be on form.html, got: %s", urlstr)
|
t.Fatalf("expected to be on form.html, got: %s", urlstr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,26 +320,35 @@ func TestLocation(t *testing.T) {
|
|||||||
func TestTitle(t *testing.T) {
|
func TestTitle(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var err error
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
expurl, exptitle := testdataDir+"/image.html", "this is title"
|
defer cancel()
|
||||||
|
|
||||||
c := testAllocate(t, "")
|
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Navigate(expurl))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var title string
|
var title string
|
||||||
err = c.Run(defaultContext, Title(&title))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||||
|
Title(&title),
|
||||||
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exptitle := "this is title"
|
||||||
if title != exptitle {
|
if title != exptitle {
|
||||||
t.Fatalf("expected title to be %s, got: %s", exptitle, title)
|
t.Fatalf("expected title to be %s, got: %s", exptitle, title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadIframe(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := testAllocate(t, "iframe.html")
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := Run(ctx, Tasks{
|
||||||
|
// TODO: remove the sleep once we have better support for
|
||||||
|
// iframes.
|
||||||
|
Sleep(10 * time.Millisecond),
|
||||||
|
// WaitVisible(`#form`, ByID), // for the nested form.html
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
219
pool.go
219
pool.go
@ -1,219 +0,0 @@
|
|||||||
package chromedp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/chromedp/chromedp/runner"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Pool manages a pool of running Chrome processes.
|
|
||||||
type Pool struct {
|
|
||||||
// start is the start port.
|
|
||||||
start int
|
|
||||||
|
|
||||||
// end is the end port.
|
|
||||||
end int
|
|
||||||
|
|
||||||
// res are the running chrome resources.
|
|
||||||
res map[int]*Res
|
|
||||||
|
|
||||||
// logging funcs
|
|
||||||
logf, debugf, errf func(string, ...interface{})
|
|
||||||
|
|
||||||
rw sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPool creates a new Chrome runner pool.
|
|
||||||
func NewPool(opts ...PoolOption) (*Pool, error) {
|
|
||||||
p := &Pool{
|
|
||||||
start: DefaultPoolStartPort,
|
|
||||||
end: DefaultPoolEndPort,
|
|
||||||
res: make(map[int]*Res),
|
|
||||||
logf: log.Printf,
|
|
||||||
debugf: func(string, ...interface{}) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply opts
|
|
||||||
for _, o := range opts {
|
|
||||||
if err := o(p); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.errf == nil {
|
|
||||||
p.errf = func(s string, v ...interface{}) {
|
|
||||||
p.logf("ERROR: "+s, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown releases all the pool resources.
|
|
||||||
func (p *Pool) Shutdown() error {
|
|
||||||
p.rw.Lock()
|
|
||||||
defer p.rw.Unlock()
|
|
||||||
|
|
||||||
for _, r := range p.res {
|
|
||||||
r.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate creates a new process runner and returns it.
|
|
||||||
func (p *Pool) Allocate(ctxt context.Context, opts ...runner.CommandLineOption) (*Res, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
r := p.next(ctxt)
|
|
||||||
|
|
||||||
// Check if the port is available first. If it's not, Chrome will print
|
|
||||||
// an "address already in use" error, but it will otherwise keep
|
|
||||||
// running. This can lead to Allocate succeeding, while the chrome
|
|
||||||
// process isn't actually listening on the port we need.
|
|
||||||
l, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", r.port))
|
|
||||||
if err != nil {
|
|
||||||
// we can't use this port, e.g. address already in use
|
|
||||||
p.errf("pool could not allocate runner on port %d: %v", r.port, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
l.Close()
|
|
||||||
|
|
||||||
p.debugf("pool allocating %d", r.port)
|
|
||||||
|
|
||||||
// create runner
|
|
||||||
r.r, err = runner.New(append([]runner.CommandLineOption{
|
|
||||||
runner.ExecPath(runner.LookChromeNames("headless_shell")),
|
|
||||||
runner.RemoteDebuggingPort(r.port),
|
|
||||||
runner.NoDefaultBrowserCheck,
|
|
||||||
runner.NoFirstRun,
|
|
||||||
runner.Headless,
|
|
||||||
}, opts...)...)
|
|
||||||
if err != nil {
|
|
||||||
defer r.Release()
|
|
||||||
p.errf("pool could not allocate runner on port %d: %v", r.port, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// start runner
|
|
||||||
err = r.r.Start(r.ctxt)
|
|
||||||
if err != nil {
|
|
||||||
defer r.Release()
|
|
||||||
p.errf("pool could not start runner on port %d: %v", r.port, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup cdp
|
|
||||||
r.c, err = New(
|
|
||||||
r.ctxt, WithRunner(r.r),
|
|
||||||
WithLogf(p.logf), WithDebugf(p.debugf), WithErrorf(p.errf),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
defer r.Release()
|
|
||||||
p.errf("pool could not connect to %d: %v", r.port, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// next returns the next available res.
|
|
||||||
func (p *Pool) next(ctxt context.Context) *Res {
|
|
||||||
p.rw.Lock()
|
|
||||||
defer p.rw.Unlock()
|
|
||||||
|
|
||||||
var found bool
|
|
||||||
var i int
|
|
||||||
for i = p.start; i < p.end; i++ {
|
|
||||||
if _, ok := p.res[i]; !ok {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
panic("no ports available")
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &Res{
|
|
||||||
p: p,
|
|
||||||
port: i,
|
|
||||||
}
|
|
||||||
r.ctxt, r.cancel = context.WithCancel(ctxt)
|
|
||||||
|
|
||||||
p.res[i] = r
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Res is a pool resource.
|
|
||||||
type Res struct {
|
|
||||||
p *Pool
|
|
||||||
ctxt context.Context
|
|
||||||
cancel func()
|
|
||||||
port int
|
|
||||||
r *runner.Runner
|
|
||||||
c *CDP
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release releases the pool resource.
|
|
||||||
func (r *Res) Release() error {
|
|
||||||
r.cancel()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if r.c != nil {
|
|
||||||
err = r.c.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.p.debugf("pool released %d", r.port)
|
|
||||||
|
|
||||||
r.p.rw.Lock()
|
|
||||||
defer r.p.rw.Unlock()
|
|
||||||
delete(r.p.res, r.port)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Port returns the allocated port for the pool resource.
|
|
||||||
func (r *Res) Port() int {
|
|
||||||
return r.port
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL returns a formatted URL for the pool resource.
|
|
||||||
func (r *Res) URL() string {
|
|
||||||
return fmt.Sprintf("http://localhost:%d/json", r.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CDP returns the actual CDP instance.
|
|
||||||
func (r *Res) CDP() *CDP {
|
|
||||||
return r.c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run runs an action.
|
|
||||||
func (r *Res) Run(ctxt context.Context, a Action) error {
|
|
||||||
return r.c.Run(ctxt, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PoolOption is a pool option.
|
|
||||||
type PoolOption func(*Pool) error
|
|
||||||
|
|
||||||
// PortRange is a pool option to set the port range to use.
|
|
||||||
func PortRange(start, end int) PoolOption {
|
|
||||||
return func(p *Pool) error {
|
|
||||||
p.start = start
|
|
||||||
p.end = end
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PoolLog is a pool option to set the logging to use for the pool.
|
|
||||||
func PoolLog(logf, debugf, errf func(string, ...interface{})) PoolOption {
|
|
||||||
return func(p *Pool) error {
|
|
||||||
p.logf, p.debugf, p.errf = logf, debugf, errf
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
47
pool_test.go
47
pool_test.go
@ -1,47 +0,0 @@
|
|||||||
package chromedp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAllocatePortInUse(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// take a random available port
|
|
||||||
l, err := net.Listen("tcp4", "localhost:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer l.Close()
|
|
||||||
|
|
||||||
ctxt, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// make the pool use the port already in use via a port range
|
|
||||||
_, portStr, _ := net.SplitHostPort(l.Addr().String())
|
|
||||||
port, _ := strconv.Atoi(portStr)
|
|
||||||
pool, err := NewPool(
|
|
||||||
PortRange(port, port+1),
|
|
||||||
// skip the error log from the used port
|
|
||||||
PoolLog(nil, nil, func(string, ...interface{}) {}),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := pool.Allocate(ctxt)
|
|
||||||
if err != nil {
|
|
||||||
want := "address already in use"
|
|
||||||
got := err.Error()
|
|
||||||
if !strings.Contains(got, want) {
|
|
||||||
t.Fatalf("wanted error to contain %q, but got %q", want, got)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.Fatal("wanted Allocate to error if port is in use")
|
|
||||||
c.Release()
|
|
||||||
}
|
|
||||||
}
|
|
99
query.go
99
query.go
@ -25,7 +25,7 @@ func Nodes(sel interface{}, nodes *[]*cdp.Node, opts ...QueryOption) Action {
|
|||||||
panic("nodes cannot be nil")
|
panic("nodes cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, n ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, n ...*cdp.Node) error {
|
||||||
*nodes = n
|
*nodes = n
|
||||||
return nil
|
return nil
|
||||||
}, opts...)
|
}, opts...)
|
||||||
@ -37,7 +37,7 @@ func NodeIDs(sel interface{}, ids *[]cdp.NodeID, opts ...QueryOption) Action {
|
|||||||
panic("nodes cannot be nil")
|
panic("nodes cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||||
nodeIDs := make([]cdp.NodeID, len(nodes))
|
nodeIDs := make([]cdp.NodeID, len(nodes))
|
||||||
for i, n := range nodes {
|
for i, n := range nodes {
|
||||||
nodeIDs[i] = n.NodeID
|
nodeIDs[i] = n.NodeID
|
||||||
@ -51,24 +51,24 @@ func NodeIDs(sel interface{}, ids *[]cdp.NodeID, opts ...QueryOption) Action {
|
|||||||
|
|
||||||
// Focus focuses the first node matching 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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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 dom.Focus().WithNodeID(nodes[0].NodeID).Do(ctxt, h)
|
return dom.Focus().WithNodeID(nodes[0].NodeID).Do(ctx, h)
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blur unfocuses (blurs) the first node matching 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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var res bool
|
var res bool
|
||||||
err := EvaluateAsDevTools(fmt.Sprintf(blurJS, nodes[0].FullXPath()), &res).Do(ctxt, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(blurJS, nodes[0].FullXPath()), &res).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -87,12 +87,12 @@ func Dimensions(sel interface{}, model **dom.BoxModel, opts ...QueryOption) Acti
|
|||||||
if model == nil {
|
if model == nil {
|
||||||
panic("model cannot be nil")
|
panic("model cannot be nil")
|
||||||
}
|
}
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
*model, err = dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctxt, h)
|
*model, err = dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctx, h)
|
||||||
return err
|
return err
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
@ -103,18 +103,18 @@ func Text(sel interface{}, text *string, opts ...QueryOption) Action {
|
|||||||
panic("text cannot be nil")
|
panic("text cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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 EvaluateAsDevTools(fmt.Sprintf(textJS, nodes[0].FullXPath()), text).Do(ctxt, h)
|
return EvaluateAsDevTools(fmt.Sprintf(textJS, nodes[0].FullXPath()), text).Do(ctx, h)
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear clears the values of any input/textarea nodes matching the selector.
|
// 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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
@ -154,7 +154,7 @@ func Clear(sel interface{}, opts ...QueryOption) Action {
|
|||||||
|
|
||||||
a = dom.SetNodeValue(textID, "")
|
a = dom.SetNodeValue(textID, "")
|
||||||
}
|
}
|
||||||
errs[i] = a.Do(ctxt, h)
|
errs[i] = a.Do(ctx, h)
|
||||||
}(i, n)
|
}(i, n)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@ -190,7 +190,7 @@ func Attributes(sel interface{}, attributes *map[string]string, opts ...QueryOpt
|
|||||||
panic("attributes cannot be nil")
|
panic("attributes cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
@ -219,7 +219,7 @@ func AttributesAll(sel interface{}, attributes *[]map[string]string, opts ...Que
|
|||||||
panic("attributes cannot be nil")
|
panic("attributes cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
@ -243,7 +243,7 @@ func AttributesAll(sel interface{}, attributes *[]map[string]string, opts ...Que
|
|||||||
// SetAttributes sets the element attributes for the first node matching the
|
// SetAttributes sets the element attributes for the first node matching the
|
||||||
// selector.
|
// 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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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")
|
||||||
}
|
}
|
||||||
@ -254,7 +254,7 @@ func SetAttributes(sel interface{}, attributes map[string]string, opts ...QueryO
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
return dom.SetAttributesAsText(nodes[0].NodeID, strings.Join(attrs, " ")).Do(ctxt, h)
|
return dom.SetAttributesAsText(nodes[0].NodeID, strings.Join(attrs, " ")).Do(ctx, h)
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,7 +265,7 @@ func AttributeValue(sel interface{}, name string, value *string, ok *bool, opts
|
|||||||
panic("value cannot be nil")
|
panic("value cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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")
|
||||||
}
|
}
|
||||||
@ -295,24 +295,24 @@ func AttributeValue(sel interface{}, name string, value *string, ok *bool, opts
|
|||||||
// SetAttributeValue sets the element attribute with name to value for the
|
// SetAttributeValue sets the element attribute with name to value for the
|
||||||
// first node matching the selector.
|
// 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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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 dom.SetAttributeValue(nodes[0].NodeID, name, value).Do(ctxt, h)
|
return dom.SetAttributeValue(nodes[0].NodeID, name, value).Do(ctx, h)
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveAttribute removes the element attribute with name from the first node
|
// RemoveAttribute removes the element attribute with name from the first node
|
||||||
// matching the selector.
|
// 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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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 dom.RemoveAttribute(nodes[0].NodeID, name).Do(ctxt, h)
|
return dom.RemoveAttribute(nodes[0].NodeID, name).Do(ctx, h)
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,25 +322,25 @@ func JavascriptAttribute(sel interface{}, name string, res interface{}, opts ...
|
|||||||
if res == nil {
|
if res == nil {
|
||||||
panic("res cannot be nil")
|
panic("res cannot be nil")
|
||||||
}
|
}
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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 EvaluateAsDevTools(fmt.Sprintf(attributeJS, nodes[0].FullXPath(), name), res).Do(ctxt, h)
|
return EvaluateAsDevTools(fmt.Sprintf(attributeJS, nodes[0].FullXPath(), name), res).Do(ctx, h)
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetJavascriptAttribute sets the javascript attribute for the first node
|
// SetJavascriptAttribute sets the javascript attribute for the first node
|
||||||
// matching the selector.
|
// matching the selector.
|
||||||
func SetJavascriptAttribute(sel interface{}, name, value string, opts ...QueryOption) Action {
|
func SetJavascriptAttribute(sel interface{}, name, value string, opts ...QueryOption) Action {
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var res string
|
var res string
|
||||||
err := EvaluateAsDevTools(fmt.Sprintf(setAttributeJS, nodes[0].FullXPath(), name, value), &res).Do(ctxt, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(setAttributeJS, nodes[0].FullXPath(), name, value), &res).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -370,24 +370,24 @@ func InnerHTML(sel interface{}, html *string, opts ...QueryOption) Action {
|
|||||||
|
|
||||||
// Click sends a mouse click event to the first node matching 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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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 MouseClickNode(nodes[0]).Do(ctxt, h)
|
return MouseClickNode(nodes[0]).Do(ctx, h)
|
||||||
}, append(opts, NodeVisible)...)
|
}, append(opts, NodeVisible)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoubleClick sends a mouse double click event to the first node matching the
|
// DoubleClick sends a mouse double click event to the first node matching the
|
||||||
// selector.
|
// selector.
|
||||||
func DoubleClick(sel interface{}, opts ...QueryOption) Action {
|
func DoubleClick(sel interface{}, opts ...QueryOption) Action {
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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 MouseClickNode(nodes[0], ClickCount(2)).Do(ctxt, h)
|
return MouseClickNode(nodes[0], ClickCount(2)).Do(ctx, h)
|
||||||
}, append(opts, NodeVisible)...)
|
}, append(opts, NodeVisible)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,7 +397,7 @@ func DoubleClick(sel interface{}, opts ...QueryOption) Action {
|
|||||||
// Note: when selector matches a input[type="file"] node, then dom.SetFileInputFiles
|
// Note: when selector matches a input[type="file"] node, then dom.SetFileInputFiles
|
||||||
// is used to set the upload path of the input node to v.
|
// is used to set the upload path of the input node to v.
|
||||||
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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
@ -416,22 +416,22 @@ func SendKeys(sel interface{}, v string, opts ...QueryOption) Action {
|
|||||||
|
|
||||||
// when working with input[type="file"], call dom.SetFileInputFiles
|
// when working with input[type="file"], call dom.SetFileInputFiles
|
||||||
if n.NodeName == "INPUT" && typ == "file" {
|
if n.NodeName == "INPUT" && typ == "file" {
|
||||||
return dom.SetFileInputFiles([]string{v}).WithNodeID(n.NodeID).Do(ctxt, h)
|
return dom.SetFileInputFiles([]string{v}).WithNodeID(n.NodeID).Do(ctx, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyActionNode(n, v).Do(ctxt, h)
|
return KeyActionNode(n, v).Do(ctx, h)
|
||||||
}, append(opts, NodeVisible)...)
|
}, append(opts, NodeVisible)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUploadFiles sets the files to upload (ie, for a input[type="file"] node)
|
// SetUploadFiles sets the files to upload (ie, for a input[type="file"] node)
|
||||||
// for the first node matching the selector.
|
// for the first node matching the selector.
|
||||||
func SetUploadFiles(sel interface{}, files []string, opts ...QueryOption) Action {
|
func SetUploadFiles(sel interface{}, files []string, opts ...QueryOption) Action {
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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 dom.SetFileInputFiles(files).WithNodeID(nodes[0].NodeID).Do(ctxt, h)
|
return dom.SetFileInputFiles(files).WithNodeID(nodes[0].NodeID).Do(ctx, h)
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,13 +441,13 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
|
|||||||
panic("picbuf cannot be nil")
|
panic("picbuf cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get box model
|
// get box model
|
||||||
box, err := dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctxt, h)
|
box, err := dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -459,13 +459,13 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
|
|||||||
|
|
||||||
// scroll to node position
|
// scroll to node position
|
||||||
var pos []int
|
var pos []int
|
||||||
err = EvaluateAsDevTools(fmt.Sprintf(scrollJS, int64(box.Margin[0]), int64(box.Margin[1])), &pos).Do(ctxt, h)
|
err = EvaluateAsDevTools(fmt.Sprintf(scrollJS, int64(box.Margin[0]), int64(box.Margin[1])), &pos).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// take page screenshot
|
// take page screenshot
|
||||||
buf, err := page.CaptureScreenshot().Do(ctxt, h)
|
buf, err := page.CaptureScreenshot().Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -484,8 +484,7 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
|
|||||||
|
|
||||||
// encode
|
// encode
|
||||||
var croppedBuf bytes.Buffer
|
var croppedBuf bytes.Buffer
|
||||||
err = png.Encode(&croppedBuf, cropped)
|
if err := png.Encode(&croppedBuf, cropped); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -498,13 +497,13 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
|
|||||||
// Submit is an action that submits the form of the first node matching the
|
// Submit is an action that submits the form of the first node matching 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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var res bool
|
var res bool
|
||||||
err := EvaluateAsDevTools(fmt.Sprintf(submitJS, nodes[0].FullXPath()), &res).Do(ctxt, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(submitJS, nodes[0].FullXPath()), &res).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -520,13 +519,13 @@ func Submit(sel interface{}, opts ...QueryOption) Action {
|
|||||||
// Reset is an action that resets the form of the first node 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 *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var res bool
|
var res bool
|
||||||
err := EvaluateAsDevTools(fmt.Sprintf(resetJS, nodes[0].FullXPath()), &res).Do(ctxt, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(resetJS, nodes[0].FullXPath()), &res).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -545,12 +544,12 @@ func ComputedStyle(sel interface{}, style *[]*css.ComputedProperty, opts ...Quer
|
|||||||
panic("style cannot be nil")
|
panic("style cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
computed, err := css.GetComputedStyleForNode(nodes[0].NodeID).Do(ctxt, h)
|
computed, err := css.GetComputedStyleForNode(nodes[0].NodeID).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -568,7 +567,7 @@ func MatchedStyle(sel interface{}, style **css.GetMatchedStylesForNodeReturns, o
|
|||||||
panic("style cannot be nil")
|
panic("style cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
@ -577,7 +576,7 @@ func MatchedStyle(sel interface{}, style **css.GetMatchedStylesForNodeReturns, o
|
|||||||
ret := &css.GetMatchedStylesForNodeReturns{}
|
ret := &css.GetMatchedStylesForNodeReturns{}
|
||||||
ret.InlineStyle, ret.AttributesStyle, ret.MatchedCSSRules,
|
ret.InlineStyle, ret.AttributesStyle, ret.MatchedCSSRules,
|
||||||
ret.PseudoElements, ret.Inherited, ret.CSSKeyframesRules,
|
ret.PseudoElements, ret.Inherited, ret.CSSKeyframesRules,
|
||||||
err = css.GetMatchedStylesForNode(nodes[0].NodeID).Do(ctxt, h)
|
err = css.GetMatchedStylesForNode(nodes[0].NodeID).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -590,13 +589,13 @@ func MatchedStyle(sel interface{}, style **css.GetMatchedStylesForNodeReturns, o
|
|||||||
|
|
||||||
// ScrollIntoView scrolls the window to the first node matching the selector.
|
// ScrollIntoView scrolls the window to the first node matching the selector.
|
||||||
func ScrollIntoView(sel interface{}, opts ...QueryOption) Action {
|
func ScrollIntoView(sel interface{}, opts ...QueryOption) Action {
|
||||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctx context.Context, h *Target, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var pos []int
|
var pos []int
|
||||||
err := EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, nodes[0].FullXPath()), &pos).Do(ctxt, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, nodes[0].FullXPath()), &pos).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
504
query_test.go
504
query_test.go
@ -1,7 +1,10 @@
|
|||||||
package chromedp
|
package chromedp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -13,15 +16,16 @@ import (
|
|||||||
"github.com/chromedp/cdproto/cdp"
|
"github.com/chromedp/cdproto/cdp"
|
||||||
"github.com/chromedp/cdproto/css"
|
"github.com/chromedp/cdproto/css"
|
||||||
"github.com/chromedp/cdproto/dom"
|
"github.com/chromedp/cdproto/dom"
|
||||||
|
"github.com/chromedp/cdproto/emulation"
|
||||||
|
|
||||||
"github.com/chromedp/chromedp/kb"
|
"git.loafle.net/commons_go/chromedp/kb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNodes(t *testing.T) {
|
func TestNodes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "table.html")
|
ctx, cancel := testAllocate(t, "table.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -34,13 +38,12 @@ func TestNodes(t *testing.T) {
|
|||||||
{"#footer", ByID, 1},
|
{"#footer", ByID, 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(nodes) != test.len {
|
if len(nodes) != test.len {
|
||||||
t.Errorf("test %d expected to have %d nodes: got %d", i, test.len, len(nodes))
|
t.Errorf("test %d expected to have %d nodes: got %d", i, test.len, len(nodes))
|
||||||
}
|
}
|
||||||
@ -50,8 +53,8 @@ func TestNodes(t *testing.T) {
|
|||||||
func TestNodeIDs(t *testing.T) {
|
func TestNodeIDs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "table.html")
|
ctx, cancel := testAllocate(t, "table.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -64,13 +67,12 @@ func TestNodeIDs(t *testing.T) {
|
|||||||
{"#footer", ByID, 1},
|
{"#footer", ByID, 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var ids []cdp.NodeID
|
var ids []cdp.NodeID
|
||||||
err = c.Run(defaultContext, NodeIDs(test.sel, &ids, test.by))
|
if err := Run(ctx, NodeIDs(test.sel, &ids, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ids) != test.len {
|
if len(ids) != test.len {
|
||||||
t.Errorf("test %d expected to have %d node id's: got %d", i, test.len, len(ids))
|
t.Errorf("test %d expected to have %d node id's: got %d", i, test.len, len(ids))
|
||||||
}
|
}
|
||||||
@ -80,8 +82,8 @@ func TestNodeIDs(t *testing.T) {
|
|||||||
func TestFocusBlur(t *testing.T) {
|
func TestFocusBlur(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -93,35 +95,29 @@ func TestFocusBlur(t *testing.T) {
|
|||||||
{"#input1", ByID},
|
{"#input1", ByID},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.Run(defaultContext, Click("#input1", ByID))
|
if err := Run(ctx, Click("#input1", ByID)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
err = c.Run(defaultContext, Focus(test.sel, test.by))
|
var value string
|
||||||
if err != nil {
|
if err := Run(ctx,
|
||||||
|
Focus(test.sel, test.by),
|
||||||
|
Value(test.sel, &value, test.by),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var value string
|
|
||||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
|
||||||
}
|
|
||||||
if value != "9999" {
|
if value != "9999" {
|
||||||
t.Errorf("test %d expected value is '9999', got: '%s'", i, value)
|
t.Errorf("test %d expected value is '9999', got: '%s'", i, value)
|
||||||
}
|
}
|
||||||
|
if err := Run(ctx,
|
||||||
err = c.Run(defaultContext, Blur(test.sel, test.by))
|
Blur(test.sel, test.by),
|
||||||
if err != nil {
|
Value(test.sel, &value, test.by),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
|
||||||
}
|
|
||||||
if value != "0" {
|
if value != "0" {
|
||||||
t.Errorf("test %d expected value is '0', got: '%s'", i, value)
|
t.Errorf("test %d expected value is '0', got: '%s'", i, value)
|
||||||
}
|
}
|
||||||
@ -131,8 +127,8 @@ func TestFocusBlur(t *testing.T) {
|
|||||||
func TestDimensions(t *testing.T) {
|
func TestDimensions(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "image.html")
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -146,13 +142,12 @@ func TestDimensions(t *testing.T) {
|
|||||||
{"#icon-github", ByID, 120, 120},
|
{"#icon-github", ByID, 120, 120},
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var model *dom.BoxModel
|
var model *dom.BoxModel
|
||||||
err = c.Run(defaultContext, Dimensions(test.sel, &model))
|
if err := Run(ctx, Dimensions(test.sel, &model)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.Height != test.height || model.Width != test.width {
|
if model.Height != test.height || model.Width != test.width {
|
||||||
t.Errorf("test %d expected %dx%d, got: %dx%d", i, test.width, test.height, model.Height, model.Width)
|
t.Errorf("test %d expected %dx%d, got: %dx%d", i, test.width, test.height, model.Height, model.Width)
|
||||||
}
|
}
|
||||||
@ -162,8 +157,8 @@ func TestDimensions(t *testing.T) {
|
|||||||
func TestText(t *testing.T) {
|
func TestText(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "form.html")
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -176,13 +171,12 @@ func TestText(t *testing.T) {
|
|||||||
{"/html/body/form/span[2]", BySearch, "keyword"},
|
{"/html/body/form/span[2]", BySearch, "keyword"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var text string
|
var text string
|
||||||
err = c.Run(defaultContext, Text(test.sel, &text, test.by))
|
if err := Run(ctx, Text(test.sel, &text, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if text != test.exp {
|
if text != test.exp {
|
||||||
t.Errorf("test %d expected `%s`, got: %s", i, test.exp, text)
|
t.Errorf("test %d expected `%s`, got: %s", i, test.exp, text)
|
||||||
}
|
}
|
||||||
@ -214,28 +208,24 @@ func TestClear(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "form.html")
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var val string
|
var val string
|
||||||
err := c.Run(defaultContext, Value(test.sel, &val, test.by))
|
if err := Run(ctx, Value(test.sel, &val, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if val == "" {
|
if val == "" {
|
||||||
t.Errorf("expected `%s` to have non empty value", test.sel)
|
t.Errorf("expected `%s` to have non empty value", test.sel)
|
||||||
}
|
}
|
||||||
|
if err := Run(ctx,
|
||||||
err = c.Run(defaultContext, Clear(test.sel, test.by))
|
Clear(test.sel, test.by),
|
||||||
if err != nil {
|
Value(test.sel, &val, test.by),
|
||||||
t.Fatalf("got error: %v", err)
|
); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Value(test.sel, &val, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if val != "" {
|
if val != "" {
|
||||||
@ -261,27 +251,22 @@ func TestReset(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "form.html")
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, SetValue(test.sel, test.value, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, Reset(test.sel, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
SetValue(test.sel, test.value, test.by),
|
||||||
|
Reset(test.sel, test.by),
|
||||||
|
Value(test.sel, &value, test.by),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != test.exp {
|
if value != test.exp {
|
||||||
t.Errorf("expected value after reset is %s, got: '%s'", test.exp, value)
|
t.Errorf("expected value after reset is %s, got: '%s'", test.exp, value)
|
||||||
}
|
}
|
||||||
@ -292,8 +277,8 @@ func TestReset(t *testing.T) {
|
|||||||
func TestValue(t *testing.T) {
|
func TestValue(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "form.html")
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -305,13 +290,12 @@ func TestValue(t *testing.T) {
|
|||||||
{`#keyword`, ByID},
|
{`#keyword`, ByID},
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != "chromedp" {
|
if value != "chromedp" {
|
||||||
t.Errorf("test %d expected `chromedp`, got: %s", i, value)
|
t.Errorf("test %d expected `chromedp`, got: %s", i, value)
|
||||||
}
|
}
|
||||||
@ -332,22 +316,21 @@ func TestSetValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "form.html")
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, SetValue(test.sel, "FOOBAR", test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
SetValue(test.sel, "FOOBAR", test.by),
|
||||||
|
Value(test.sel, &value, test.by),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != "FOOBAR" {
|
if value != "FOOBAR" {
|
||||||
t.Errorf("expected `FOOBAR`, got: %s", value)
|
t.Errorf("expected `FOOBAR`, got: %s", value)
|
||||||
}
|
}
|
||||||
@ -358,45 +341,51 @@ func TestSetValue(t *testing.T) {
|
|||||||
func TestAttributes(t *testing.T) {
|
func TestAttributes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "image.html")
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
by QueryOption
|
by QueryOption
|
||||||
exp map[string]string
|
exp map[string]string
|
||||||
}{
|
}{
|
||||||
{`//*[@id="icon-brankas"]`, BySearch,
|
{
|
||||||
|
`//*[@id="icon-brankas"]`, BySearch,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"alt": "Brankas - Easy Money Management",
|
"alt": "Brankas - Easy Money Management",
|
||||||
"id": "icon-brankas",
|
"id": "icon-brankas",
|
||||||
"src": "images/brankas.png",
|
"src": "images/brankas.png",
|
||||||
}},
|
},
|
||||||
{"body > img:first-child", ByQuery,
|
},
|
||||||
|
{
|
||||||
|
"body > img:first-child", ByQuery,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"alt": "Brankas - Easy Money Management",
|
"alt": "Brankas - Easy Money Management",
|
||||||
"id": "icon-brankas",
|
"id": "icon-brankas",
|
||||||
"src": "images/brankas.png",
|
"src": "images/brankas.png",
|
||||||
}},
|
},
|
||||||
{"body > img:nth-child(2)", ByQueryAll,
|
},
|
||||||
|
{
|
||||||
|
"body > img:nth-child(2)", ByQueryAll,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"alt": `How people build software`,
|
"alt": `How people build software`,
|
||||||
"id": "icon-github",
|
"id": "icon-github",
|
||||||
"src": "images/github.png",
|
"src": "images/github.png",
|
||||||
}},
|
},
|
||||||
{"#icon-github", ByID,
|
},
|
||||||
|
{
|
||||||
|
"#icon-github", ByID,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"alt": "How people build software",
|
"alt": "How people build software",
|
||||||
"id": "icon-github",
|
"id": "icon-github",
|
||||||
"src": "images/github.png",
|
"src": "images/github.png",
|
||||||
}},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var attrs map[string]string
|
var attrs map[string]string
|
||||||
err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by))
|
if err := Run(ctx, Attributes(test.sel, &attrs, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,15 +398,16 @@ func TestAttributes(t *testing.T) {
|
|||||||
func TestAttributesAll(t *testing.T) {
|
func TestAttributesAll(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "image.html")
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
by QueryOption
|
by QueryOption
|
||||||
exp []map[string]string
|
exp []map[string]string
|
||||||
}{
|
}{
|
||||||
{"img", ByQueryAll,
|
{
|
||||||
|
"img", ByQueryAll,
|
||||||
[]map[string]string{
|
[]map[string]string{
|
||||||
{
|
{
|
||||||
"alt": "Brankas - Easy Money Management",
|
"alt": "Brankas - Easy Money Management",
|
||||||
@ -433,11 +423,9 @@ func TestAttributesAll(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var attrs []map[string]string
|
var attrs []map[string]string
|
||||||
err = c.Run(defaultContext, AttributesAll(test.sel, &attrs, test.by))
|
if err := Run(ctx, AttributesAll(test.sel, &attrs, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,22 +444,28 @@ func TestSetAttributes(t *testing.T) {
|
|||||||
attrs map[string]string
|
attrs map[string]string
|
||||||
exp map[string]string
|
exp map[string]string
|
||||||
}{
|
}{
|
||||||
{`//*[@id="icon-brankas"]`, BySearch,
|
{
|
||||||
map[string]string{"data-url": "brankas"},
|
`//*[@id="icon-brankas"]`, BySearch,
|
||||||
map[string]string{
|
|
||||||
"alt": "Brankas - Easy Money Management",
|
|
||||||
"id": "icon-brankas",
|
|
||||||
"src": "images/brankas.png",
|
|
||||||
"data-url": "brankas"}},
|
|
||||||
{"body > img:first-child", ByQuery,
|
|
||||||
map[string]string{"data-url": "brankas"},
|
map[string]string{"data-url": "brankas"},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"alt": "Brankas - Easy Money Management",
|
"alt": "Brankas - Easy Money Management",
|
||||||
"id": "icon-brankas",
|
"id": "icon-brankas",
|
||||||
"src": "images/brankas.png",
|
"src": "images/brankas.png",
|
||||||
"data-url": "brankas",
|
"data-url": "brankas",
|
||||||
}},
|
},
|
||||||
{"body > img:nth-child(2)", ByQueryAll,
|
},
|
||||||
|
{
|
||||||
|
"body > img:first-child", ByQuery,
|
||||||
|
map[string]string{"data-url": "brankas"},
|
||||||
|
map[string]string{
|
||||||
|
"alt": "Brankas - Easy Money Management",
|
||||||
|
"id": "icon-brankas",
|
||||||
|
"src": "images/brankas.png",
|
||||||
|
"data-url": "brankas",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body > img:nth-child(2)", ByQueryAll,
|
||||||
map[string]string{"width": "100", "height": "200"},
|
map[string]string{"width": "100", "height": "200"},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"alt": `How people build software`,
|
"alt": `How people build software`,
|
||||||
@ -479,8 +473,10 @@ func TestSetAttributes(t *testing.T) {
|
|||||||
"src": "images/github.png",
|
"src": "images/github.png",
|
||||||
"width": "100",
|
"width": "100",
|
||||||
"height": "200",
|
"height": "200",
|
||||||
}},
|
},
|
||||||
{"#icon-github", ByID,
|
},
|
||||||
|
{
|
||||||
|
"#icon-github", ByID,
|
||||||
map[string]string{"width": "100", "height": "200"},
|
map[string]string{"width": "100", "height": "200"},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"alt": "How people build software",
|
"alt": "How people build software",
|
||||||
@ -488,24 +484,27 @@ func TestSetAttributes(t *testing.T) {
|
|||||||
"src": "images/github.png",
|
"src": "images/github.png",
|
||||||
"width": "100",
|
"width": "100",
|
||||||
"height": "200",
|
"height": "200",
|
||||||
}},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "image.html")
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, SetAttributes(test.sel, test.attrs, test.by))
|
if err := Run(ctx, SetAttributes(test.sel, test.attrs, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: figure why this test is flaky without this
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
var attrs map[string]string
|
var attrs map[string]string
|
||||||
err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by))
|
if err := Run(ctx, Attributes(test.sel, &attrs, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,8 +518,8 @@ func TestSetAttributes(t *testing.T) {
|
|||||||
func TestAttributeValue(t *testing.T) {
|
func TestAttributeValue(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "image.html")
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -534,20 +533,15 @@ func TestAttributeValue(t *testing.T) {
|
|||||||
{"#icon-github", ByID, "alt", "How people build software"},
|
{"#icon-github", ByID, "alt", "How people build software"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var value string
|
var value string
|
||||||
var ok bool
|
var ok bool
|
||||||
|
if err := Run(ctx, AttributeValue(test.sel, test.attr, &value, &ok, test.by)); err != nil {
|
||||||
err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("test %d failed to get attribute %s on %s", i, test.attr, test.sel)
|
t.Fatalf("test %d failed to get attribute %s on %s", i, test.attr, test.sel)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != test.exp {
|
if value != test.exp {
|
||||||
t.Errorf("test %d expected %s to be %s, got: %s", i, test.attr, test.exp, value)
|
t.Errorf("test %d expected %s to be %s, got: %s", i, test.attr, test.exp, value)
|
||||||
}
|
}
|
||||||
@ -570,27 +564,28 @@ func TestSetAttributeValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "form.html")
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, SetAttributeValue(test.sel, test.attr, test.exp, test.by))
|
if err := Run(ctx, SetAttributeValue(test.sel, test.attr, test.exp, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: figure why this test is flaky without this
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
var ok bool
|
var ok bool
|
||||||
err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by))
|
if err := Run(ctx, AttributeValue(test.sel, test.attr, &value, &ok, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("failed to get attribute %s on %s", test.attr, test.sel)
|
t.Fatalf("failed to get attribute %s on %s", test.attr, test.sel)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != test.exp {
|
if value != test.exp {
|
||||||
t.Errorf("expected %s to be %s, got: %s", test.attr, test.exp, value)
|
t.Errorf("expected %s to be %s, got: %s", test.attr, test.exp, value)
|
||||||
}
|
}
|
||||||
@ -613,21 +608,23 @@ func TestRemoveAttribute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "image.html")
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, RemoveAttribute(test.sel, test.attr))
|
if err := Run(ctx, RemoveAttribute(test.sel, test.attr)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: figure why this test is flaky without this
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
var ok bool
|
var ok bool
|
||||||
err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by))
|
if err := Run(ctx, AttributeValue(test.sel, test.attr, &value, &ok, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if ok || value != "" {
|
if ok || value != "" {
|
||||||
@ -651,27 +648,22 @@ func TestClick(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "form.html")
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, Click(test.sel, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitVisible("#icon-brankas", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var title string
|
var title string
|
||||||
err = c.Run(defaultContext, Title(&title))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
Click(test.sel, test.by),
|
||||||
|
WaitVisible("#icon-brankas", ByID),
|
||||||
|
Title(&title),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if title != "this is title" {
|
if title != "this is title" {
|
||||||
t.Errorf("expected title to be 'chromedp - Google Search', got: '%s'", title)
|
t.Errorf("expected title to be 'chromedp - Google Search', got: '%s'", title)
|
||||||
}
|
}
|
||||||
@ -693,24 +685,21 @@ func TestDoubleClick(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, DoubleClick(test.sel, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value("#input1", &value, ByID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
DoubleClick(test.sel, test.by),
|
||||||
|
Value("#input1", &value, ByID),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != "1" {
|
if value != "1" {
|
||||||
t.Errorf("expected value to be '1', got: '%s'", value)
|
t.Errorf("expected value to be '1', got: '%s'", value)
|
||||||
}
|
}
|
||||||
@ -736,22 +725,21 @@ func TestSendKeys(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "visible.html")
|
ctx, cancel := testAllocate(t, "visible.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, SendKeys(test.sel, test.keys, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var val string
|
var val string
|
||||||
err = c.Run(defaultContext, Value(test.sel, &val, test.by))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
SendKeys(test.sel, test.keys, test.by),
|
||||||
|
Value(test.sel, &val, test.by),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if val != test.exp {
|
if val != test.exp {
|
||||||
t.Errorf("expected value %s, got: %s", test.exp, val)
|
t.Errorf("expected value %s, got: %s", test.exp, val)
|
||||||
}
|
}
|
||||||
@ -762,31 +750,47 @@ func TestSendKeys(t *testing.T) {
|
|||||||
func TestScreenshot(t *testing.T) {
|
func TestScreenshot(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "image.html")
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
by QueryOption
|
by QueryOption
|
||||||
|
size int
|
||||||
}{
|
}{
|
||||||
{"/html/body/img", BySearch},
|
{"/html/body/img", BySearch, 239},
|
||||||
{"img", ByQueryAll},
|
{"img", ByQueryAll, 239},
|
||||||
{"img", ByQuery},
|
{"#icon-github", ByID, 120},
|
||||||
{"#icon-github", ByID},
|
}
|
||||||
|
|
||||||
|
// a smaller viewport speeds up this test
|
||||||
|
width, height := 650, 450
|
||||||
|
if err := Run(ctx, emulation.SetDeviceMetricsOverride(
|
||||||
|
int64(width), int64(height), 1.0, false,
|
||||||
|
)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var buf []byte
|
var buf []byte
|
||||||
err = c.Run(defaultContext, Screenshot(test.sel, &buf))
|
if err := Run(ctx, Screenshot(test.sel, &buf)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
t.Fatalf("test %d failed to capture screenshot", i)
|
t.Fatalf("test %d failed to capture screenshot", i)
|
||||||
}
|
}
|
||||||
//TODO: test image
|
config, format, err := image.DecodeConfig(bytes.NewReader(buf))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if want := "png"; format != want {
|
||||||
|
t.Fatalf("expected format to be %q, got %q", want, format)
|
||||||
|
}
|
||||||
|
if config.Width != test.size || config.Height != test.size {
|
||||||
|
t.Fatalf("expected dimensions to be %d*%d, got %d*%d",
|
||||||
|
test.size, test.size, config.Width, config.Height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -804,27 +808,22 @@ func TestSubmit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "form.html")
|
ctx, cancel := testAllocate(t, "form.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, Submit(test.sel, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitVisible("#icon-brankas", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var title string
|
var title string
|
||||||
err = c.Run(defaultContext, Title(&title))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
Submit(test.sel, test.by),
|
||||||
|
WaitVisible("#icon-brankas", ByID),
|
||||||
|
Title(&title),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if title != "this is title" {
|
if title != "this is title" {
|
||||||
t.Errorf("expected title to be 'this is title', got: '%s'", title)
|
t.Errorf("expected title to be 'this is title', got: '%s'", title)
|
||||||
}
|
}
|
||||||
@ -846,17 +845,15 @@ func TestComputedStyle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var styles []*css.ComputedProperty
|
var styles []*css.ComputedProperty
|
||||||
err := c.Run(defaultContext, ComputedStyle(test.sel, &styles, test.by))
|
if err := Run(ctx, ComputedStyle(test.sel, &styles, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -867,16 +864,10 @@ func TestComputedStyle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := Run(ctx,
|
||||||
err = c.Run(defaultContext, Click("#input1", ByID))
|
Click("#input1", ByID),
|
||||||
if err != nil {
|
ComputedStyle(test.sel, &styles, test.by),
|
||||||
t.Fatalf("got error: %v", err)
|
); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, ComputedStyle(test.sel, &styles, test.by))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -905,17 +896,15 @@ func TestMatchedStyle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
test := test
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
var styles *css.GetMatchedStylesForNodeReturns
|
var styles *css.GetMatchedStylesForNodeReturns
|
||||||
err := c.Run(defaultContext, MatchedStyle(test.sel, &styles, test.by))
|
if err := Run(ctx, MatchedStyle(test.sel, &styles, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -930,7 +919,7 @@ func TestFileUpload(t *testing.T) {
|
|||||||
// create test server
|
// create test server
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
||||||
fmt.Fprintf(res, uploadHTML)
|
fmt.Fprintf(res, "%s", uploadHTML)
|
||||||
})
|
})
|
||||||
mux.HandleFunc("/upload", func(res http.ResponseWriter, req *http.Request) {
|
mux.HandleFunc("/upload", func(res http.ResponseWriter, req *http.Request) {
|
||||||
f, _, err := req.FormFile("upload")
|
f, _, err := req.FormFile("upload")
|
||||||
@ -957,10 +946,11 @@ func TestFileUpload(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.Remove(tmpfile.Name())
|
defer os.Remove(tmpfile.Name())
|
||||||
if _, err = tmpfile.WriteString(uploadHTML); err != nil {
|
defer tmpfile.Close()
|
||||||
|
if _, err := tmpfile.WriteString(uploadHTML); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err = tmpfile.Close(); err != nil {
|
if err := tmpfile.Close(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -971,25 +961,26 @@ func TestFileUpload(t *testing.T) {
|
|||||||
{SetUploadFiles(`input[name="upload"]`, []string{tmpfile.Name()}, NodeVisible)},
|
{SetUploadFiles(`input[name="upload"]`, []string{tmpfile.Name()}, NodeVisible)},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't run these tests in parallel. The only way to do so would be to
|
||||||
|
// fire a separate httptest server and tmpfile for each. There's no way
|
||||||
|
// to share these resources easily among parallel subtests, as the
|
||||||
|
// parent must finish for the children to run, made impossible by the
|
||||||
|
// defers above.
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
// TODO: refactor the test so the subtests can run in
|
ctx, cancel := testAllocate(t, "")
|
||||||
// parallel
|
defer cancel()
|
||||||
//t.Parallel()
|
|
||||||
|
|
||||||
c := testAllocate(t, "")
|
|
||||||
defer c.Release()
|
|
||||||
|
|
||||||
var result string
|
var result string
|
||||||
err = c.Run(defaultContext, Tasks{
|
if err := Run(ctx,
|
||||||
Navigate(s.URL),
|
Navigate(s.URL),
|
||||||
test.a,
|
test.a,
|
||||||
Click(`input[name="submit"]`),
|
Click(`input[name="submit"]`),
|
||||||
Text(`#result`, &result, ByID, NodeVisible),
|
Text(`#result`, &result, ByID, NodeVisible),
|
||||||
})
|
); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d expected no error, got: %v", i, err)
|
t.Fatalf("test %d expected no error, got: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result != fmt.Sprintf("%d", len(uploadHTML)) {
|
if result != fmt.Sprintf("%d", len(uploadHTML)) {
|
||||||
t.Errorf("test %d expected result to be %d, got: %s", i, len(uploadHTML), result)
|
t.Errorf("test %d expected result to be %d, got: %s", i, len(uploadHTML), result)
|
||||||
}
|
}
|
||||||
@ -1000,8 +991,8 @@ func TestFileUpload(t *testing.T) {
|
|||||||
func TestInnerHTML(t *testing.T) {
|
func TestInnerHTML(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "table.html")
|
ctx, cancel := testAllocate(t, "table.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -1011,13 +1002,12 @@ func TestInnerHTML(t *testing.T) {
|
|||||||
{"thead", ByQueryAll},
|
{"thead", ByQueryAll},
|
||||||
{"thead", ByQuery},
|
{"thead", ByQuery},
|
||||||
}
|
}
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var html string
|
var html string
|
||||||
err = c.Run(defaultContext, InnerHTML(test.sel, &html))
|
if err := Run(ctx, InnerHTML(test.sel, &html)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if html == "" {
|
if html == "" {
|
||||||
t.Fatalf("test %d: InnerHTML is empty", i)
|
t.Fatalf("test %d: InnerHTML is empty", i)
|
||||||
}
|
}
|
||||||
@ -1027,8 +1017,8 @@ func TestInnerHTML(t *testing.T) {
|
|||||||
func TestOuterHTML(t *testing.T) {
|
func TestOuterHTML(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "table.html")
|
ctx, cancel := testAllocate(t, "table.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -1038,13 +1028,12 @@ func TestOuterHTML(t *testing.T) {
|
|||||||
{"thead tr", ByQueryAll},
|
{"thead tr", ByQueryAll},
|
||||||
{"thead tr", ByQuery},
|
{"thead tr", ByQuery},
|
||||||
}
|
}
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var html string
|
var html string
|
||||||
err = c.Run(defaultContext, OuterHTML(test.sel, &html))
|
if err := Run(ctx, OuterHTML(test.sel, &html)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if html == "" {
|
if html == "" {
|
||||||
t.Fatalf("test %d: OuterHTML is empty", i)
|
t.Fatalf("test %d: OuterHTML is empty", i)
|
||||||
}
|
}
|
||||||
@ -1054,8 +1043,8 @@ func TestOuterHTML(t *testing.T) {
|
|||||||
func TestScrollIntoView(t *testing.T) {
|
func TestScrollIntoView(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "image.html")
|
ctx, cancel := testAllocate(t, "image.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -1066,12 +1055,11 @@ func TestScrollIntoView(t *testing.T) {
|
|||||||
{"img", ByQuery},
|
{"img", ByQuery},
|
||||||
{"#icon-github", ByID},
|
{"#icon-github", ByID},
|
||||||
}
|
}
|
||||||
var err error
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
err = c.Run(defaultContext, ScrollIntoView(test.sel, test.by))
|
if err := Run(ctx, ScrollIntoView(test.sel, test.by)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO test scroll event
|
// TODO test scroll event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
// +build darwin
|
|
||||||
|
|
||||||
package runner
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultChromePath is the default path to use for Chrome if the
|
|
||||||
// executable is not in $PATH.
|
|
||||||
DefaultChromePath = `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultChromeNames are the default Chrome executable names to look for in
|
|
||||||
// $PATH.
|
|
||||||
var DefaultChromeNames []string
|
|
@ -1,19 +0,0 @@
|
|||||||
// +build linux freebsd netbsd openbsd
|
|
||||||
|
|
||||||
package runner
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultChromePath is the default path to use for Chrome if the
|
|
||||||
// executable is not in $PATH.
|
|
||||||
DefaultChromePath = "/usr/bin/google-chrome"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultChromeNames are the default Chrome executable names to look for in
|
|
||||||
// $PATH.
|
|
||||||
var DefaultChromeNames = []string{
|
|
||||||
"google-chrome",
|
|
||||||
"chromium-browser",
|
|
||||||
"chromium",
|
|
||||||
"google-chrome-beta",
|
|
||||||
"google-chrome-unstable",
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
// +build windows
|
|
||||||
|
|
||||||
package runner
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultChromePath is the default path to use for Chrome if the
|
|
||||||
// executable is not in %PATH%.
|
|
||||||
DefaultChromePath = `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultChromeNames are the default Chrome executable names to look for in
|
|
||||||
// %PATH%.
|
|
||||||
var DefaultChromeNames = []string{`chrome.exe`}
|
|
482
runner/runner.go
482
runner/runner.go
@ -1,482 +0,0 @@
|
|||||||
// Package runner provides a Chrome process runner.
|
|
||||||
package runner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/chromedp/chromedp/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultUserDataDirPrefix is the default user data directory prefix.
|
|
||||||
DefaultUserDataDirPrefix = "chromedp-runner.%d."
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error is a runner error.
|
|
||||||
type Error string
|
|
||||||
|
|
||||||
// Error satisfies the error interface.
|
|
||||||
func (err Error) Error() string {
|
|
||||||
return string(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error values.
|
|
||||||
const (
|
|
||||||
// ErrAlreadyStarted is the already started error.
|
|
||||||
ErrAlreadyStarted Error = "already started"
|
|
||||||
|
|
||||||
// ErrAlreadyWaiting is the already waiting error.
|
|
||||||
ErrAlreadyWaiting Error = "already waiting"
|
|
||||||
|
|
||||||
// ErrInvalidURLs is the invalid url-opts error.
|
|
||||||
ErrInvalidURLOpts Error = "invalid url-opts"
|
|
||||||
|
|
||||||
// ErrInvalidCmdOpts is the invalid cmd-opts error.
|
|
||||||
ErrInvalidCmdOpts Error = "invalid cmd-opts"
|
|
||||||
|
|
||||||
// ErrInvalidProcessOpts is the invalid process-opts error.
|
|
||||||
ErrInvalidProcessOpts Error = "invalid process-opts"
|
|
||||||
|
|
||||||
// ErrInvalidExecPath is the invalid exec-path error.
|
|
||||||
ErrInvalidExecPath Error = "invalid exec-path"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Runner holds information about a running Chrome process.
|
|
||||||
type Runner struct {
|
|
||||||
opts map[string]interface{}
|
|
||||||
cmd *exec.Cmd
|
|
||||||
waiting bool
|
|
||||||
rw sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new Chrome process using the supplied command line options.
|
|
||||||
func New(opts ...CommandLineOption) (*Runner, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
cliOpts := make(map[string]interface{})
|
|
||||||
|
|
||||||
// apply opts
|
|
||||||
for _, o := range opts {
|
|
||||||
if err = o(cliOpts); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set default Chrome options if exec-path not provided
|
|
||||||
if _, ok := cliOpts["exec-path"]; !ok {
|
|
||||||
cliOpts["exec-path"] = LookChromeNames()
|
|
||||||
for k, v := range map[string]interface{}{
|
|
||||||
"no-first-run": true,
|
|
||||||
"no-default-browser-check": true,
|
|
||||||
"remote-debugging-port": 9222,
|
|
||||||
} {
|
|
||||||
if _, ok := cliOpts[k]; !ok {
|
|
||||||
cliOpts[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add KillProcessGroup and ForceKill if no other cmd opts provided
|
|
||||||
if _, ok := cliOpts["cmd-opts"]; !ok {
|
|
||||||
for _, o := range []CommandLineOption{KillProcessGroup, ForceKill} {
|
|
||||||
if err = o(cliOpts); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Runner{
|
|
||||||
opts: cliOpts,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cliOptRE is a regular expression to validate a chrome cli option.
|
|
||||||
var cliOptRE = regexp.MustCompile(`^[a-z0-9\-]+$`)
|
|
||||||
|
|
||||||
// buildOpts generates the command line options for Chrome.
|
|
||||||
func (r *Runner) buildOpts() []string {
|
|
||||||
var opts []string
|
|
||||||
var urls []string
|
|
||||||
|
|
||||||
// process opts
|
|
||||||
for k, v := range r.opts {
|
|
||||||
if !cliOptRE.MatchString(k) || v == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch k {
|
|
||||||
case "exec-path", "cmd-opts", "process-opts":
|
|
||||||
continue
|
|
||||||
|
|
||||||
case "url-opts":
|
|
||||||
urls = v.([]string)
|
|
||||||
|
|
||||||
default:
|
|
||||||
switch z := v.(type) {
|
|
||||||
case bool:
|
|
||||||
if z {
|
|
||||||
opts = append(opts, "--"+k)
|
|
||||||
}
|
|
||||||
|
|
||||||
case string:
|
|
||||||
opts = append(opts, "--"+k+"="+z)
|
|
||||||
|
|
||||||
default:
|
|
||||||
opts = append(opts, "--"+k+"="+fmt.Sprintf("%v", v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if urls == nil {
|
|
||||||
urls = append(urls, "about:blank")
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(opts, urls...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts a Chrome process using the specified context. The Chrome
|
|
||||||
// process can be terminated by closing the passed context.
|
|
||||||
func (r *Runner) Start(ctxt context.Context, opts ...string) error {
|
|
||||||
var err error
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
r.rw.RLock()
|
|
||||||
cmd := r.cmd
|
|
||||||
r.rw.RUnlock()
|
|
||||||
|
|
||||||
if cmd != nil {
|
|
||||||
return ErrAlreadyStarted
|
|
||||||
}
|
|
||||||
|
|
||||||
// set user data dir, if not provided
|
|
||||||
_, ok = r.opts["user-data-dir"]
|
|
||||||
if !ok {
|
|
||||||
r.opts["user-data-dir"], err = ioutil.TempDir(
|
|
||||||
defaultUserDataTmpDir, fmt.Sprintf(DefaultUserDataDirPrefix, r.Port()),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get exec path
|
|
||||||
var execPath string
|
|
||||||
if p, ok := r.opts["exec-path"]; ok {
|
|
||||||
execPath, ok = p.(string)
|
|
||||||
if !ok {
|
|
||||||
return ErrInvalidExecPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure execPath is valid
|
|
||||||
if execPath == "" {
|
|
||||||
return ErrInvalidExecPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// create cmd
|
|
||||||
r.cmd = exec.CommandContext(ctxt, execPath, append(r.buildOpts(), opts...)...)
|
|
||||||
|
|
||||||
// apply cmd opts
|
|
||||||
if cmdOpts, ok := r.opts["cmd-opts"]; ok {
|
|
||||||
for _, co := range cmdOpts.([]func(*exec.Cmd) error) {
|
|
||||||
if err = co(r.cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start process
|
|
||||||
if err = r.cmd.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply process opts
|
|
||||||
if processOpts, ok := r.opts["process-opts"]; ok {
|
|
||||||
for _, po := range processOpts.([]func(*os.Process) error) {
|
|
||||||
if err = po(r.cmd.Process); err != nil {
|
|
||||||
// TODO: do something better here, as we want to kill
|
|
||||||
// the child process, do cleanup, etc.
|
|
||||||
panic(err)
|
|
||||||
//return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown shuts down the Chrome process.
|
|
||||||
func (r *Runner) Shutdown(ctxt context.Context, opts ...client.Option) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
cl := r.Client(opts...)
|
|
||||||
|
|
||||||
targets, err := cl.ListPageTargets(ctxt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
errs := make([]error, len(targets))
|
|
||||||
for i, t := range targets {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(wg *sync.WaitGroup, i int, t client.Target) {
|
|
||||||
defer wg.Done()
|
|
||||||
errs[i] = cl.CloseTarget(ctxt, t)
|
|
||||||
}(&wg, i, t)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
for _, e := range errs {
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// osx applications do not automatically exit when all windows (ie, tabs)
|
|
||||||
// closed, so send SIGTERM.
|
|
||||||
//
|
|
||||||
// TODO: add other behavior here for more process options on shutdown?
|
|
||||||
if runtime.GOOS == "darwin" && r.cmd != nil && r.cmd.Process != nil {
|
|
||||||
return r.cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait waits for the previously started Chrome process to terminate, returning
|
|
||||||
// any encountered error.
|
|
||||||
func (r *Runner) Wait() error {
|
|
||||||
r.rw.RLock()
|
|
||||||
waiting := r.waiting
|
|
||||||
r.rw.RUnlock()
|
|
||||||
|
|
||||||
if waiting {
|
|
||||||
return ErrAlreadyWaiting
|
|
||||||
}
|
|
||||||
|
|
||||||
r.rw.Lock()
|
|
||||||
r.waiting = true
|
|
||||||
r.rw.Unlock()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
r.rw.Lock()
|
|
||||||
r.waiting = false
|
|
||||||
r.rw.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return r.cmd.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Port returns the port the process was launched with.
|
|
||||||
func (r *Runner) Port() int {
|
|
||||||
var port interface{}
|
|
||||||
var ok bool
|
|
||||||
port, ok = r.opts["remote-debugging-port"]
|
|
||||||
if !ok {
|
|
||||||
port, ok = r.opts["port"]
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
panic("expected either remote-debugging-port or port to be specified in command line options")
|
|
||||||
}
|
|
||||||
|
|
||||||
var p int
|
|
||||||
p, ok = port.(int)
|
|
||||||
if !ok {
|
|
||||||
panic("expected port to be type int")
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client returns a Chrome DevTools Protocol client for the running Chrome
|
|
||||||
// process.
|
|
||||||
func (r *Runner) Client(opts ...client.Option) *client.Client {
|
|
||||||
return client.New(append(opts,
|
|
||||||
client.URL(fmt.Sprintf("http://localhost:%d/json", r.Port())),
|
|
||||||
)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run starts a new Chrome process runner, using the provided context and
|
|
||||||
// command line options.
|
|
||||||
func Run(ctxt context.Context, opts ...CommandLineOption) (*Runner, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// create
|
|
||||||
r, err := New(opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// start
|
|
||||||
if err = r.Start(ctxt); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommandLineOption is a runner command line option.
|
|
||||||
//
|
|
||||||
// see: http://peter.sh/experiments/chromium-command-line-switches/
|
|
||||||
type CommandLineOption func(map[string]interface{}) error
|
|
||||||
|
|
||||||
// Flag is a generic command line option to pass a name=value flag to
|
|
||||||
// Chrome.
|
|
||||||
func Flag(name string, value interface{}) CommandLineOption {
|
|
||||||
return func(m map[string]interface{}) error {
|
|
||||||
m[name] = value
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path sets the path to the Chrome executable and sets default run options for
|
|
||||||
// Chrome. This will also set the remote debugging port to 9222, and disable
|
|
||||||
// the first run / default browser check.
|
|
||||||
//
|
|
||||||
// Note: use ExecPath if you do not want to set other options.
|
|
||||||
func Path(path string) CommandLineOption {
|
|
||||||
return func(m map[string]interface{}) error {
|
|
||||||
m["exec-path"] = path
|
|
||||||
m["no-first-run"] = true
|
|
||||||
m["no-default-browser-check"] = true
|
|
||||||
m["remote-debugging-port"] = 9222
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecPath is a command line option to set the exec path.
|
|
||||||
func ExecPath(path string) CommandLineOption {
|
|
||||||
return Flag("exec-path", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserDataDir is the command line option to set the user data dir.
|
|
||||||
//
|
|
||||||
// Note: set this option to manually set the profile directory used by Chrome.
|
|
||||||
// When this is not set, then a default path will be created in the /tmp
|
|
||||||
// directory.
|
|
||||||
func UserDataDir(dir string) CommandLineOption {
|
|
||||||
return Flag("user-data-dir", dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProxyServer is the command line option to set the outbound proxy server.
|
|
||||||
func ProxyServer(proxy string) CommandLineOption {
|
|
||||||
return Flag("proxy-server", proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WindowSize is the command line option to set the initial window size.
|
|
||||||
func WindowSize(width, height int) CommandLineOption {
|
|
||||||
return Flag("window-size", fmt.Sprintf("%d,%d", width, height))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserAgent is the command line option to set the default User-Agent
|
|
||||||
// header.
|
|
||||||
func UserAgent(userAgent string) CommandLineOption {
|
|
||||||
return Flag("user-agent", userAgent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoSandbox is the Chrome comamnd line option to disable the sandbox.
|
|
||||||
func NoSandbox(m map[string]interface{}) error {
|
|
||||||
return Flag("no-sandbox", true)(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoFirstRun is the Chrome comamnd line option to disable the first run
|
|
||||||
// dialog.
|
|
||||||
func NoFirstRun(m map[string]interface{}) error {
|
|
||||||
return Flag("no-first-run", true)(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoDefaultBrowserCheck is the Chrome comamnd line option to disable the
|
|
||||||
// default browser check.
|
|
||||||
func NoDefaultBrowserCheck(m map[string]interface{}) error {
|
|
||||||
return Flag("no-default-browser-check", true)(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteDebuggingPort is the command line option to set the remote
|
|
||||||
// debugging port.
|
|
||||||
func RemoteDebuggingPort(port int) CommandLineOption {
|
|
||||||
return Flag("remote-debugging-port", port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headless is the command line option to run in headless mode.
|
|
||||||
func Headless(m map[string]interface{}) error {
|
|
||||||
return Flag("headless", true)(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisableGPU is the command line option to disable the GPU process.
|
|
||||||
func DisableGPU(m map[string]interface{}) error {
|
|
||||||
return Flag("disable-gpu", true)(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL is the command line option to add a URL to open on process start.
|
|
||||||
//
|
|
||||||
// Note: this can be specified multiple times, and each URL will be opened in a
|
|
||||||
// new tab.
|
|
||||||
func URL(urlstr string) CommandLineOption {
|
|
||||||
return func(m map[string]interface{}) error {
|
|
||||||
var urls []string
|
|
||||||
if u, ok := m["url-opts"]; ok {
|
|
||||||
urls, ok = u.([]string)
|
|
||||||
if !ok {
|
|
||||||
return ErrInvalidURLOpts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m["url-opts"] = append(urls, urlstr)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdOpt is a command line option to modify the underlying exec.Cmd
|
|
||||||
// prior to the call to exec.Cmd.Start in Run.
|
|
||||||
func CmdOpt(o func(*exec.Cmd) error) CommandLineOption {
|
|
||||||
return func(m map[string]interface{}) error {
|
|
||||||
var opts []func(*exec.Cmd) error
|
|
||||||
if e, ok := m["cmd-opts"]; ok {
|
|
||||||
opts, ok = e.([]func(*exec.Cmd) error)
|
|
||||||
if !ok {
|
|
||||||
return ErrInvalidCmdOpts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m["cmd-opts"] = append(opts, o)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessOpt is a command line option to modify the child os.Process
|
|
||||||
// after the call to exec.Cmd.Start in Run.
|
|
||||||
func ProcessOpt(o func(*os.Process) error) CommandLineOption {
|
|
||||||
return func(m map[string]interface{}) error {
|
|
||||||
var opts []func(*os.Process) error
|
|
||||||
if e, ok := m["process-opts"]; ok {
|
|
||||||
opts, ok = e.([]func(*os.Process) error)
|
|
||||||
if !ok {
|
|
||||||
return ErrInvalidProcessOpts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m["process-opts"] = append(opts, o)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookChromeNames looks for the platform's DefaultChromeNames and any
|
|
||||||
// additional names using exec.LookPath, returning the first encountered
|
|
||||||
// location or the platform's DefaultChromePath if no names are found on the
|
|
||||||
// path.
|
|
||||||
func LookChromeNames(additional ...string) string {
|
|
||||||
for _, p := range append(additional, DefaultChromeNames...) {
|
|
||||||
path, err := exec.LookPath(p)
|
|
||||||
if err == nil {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefaultChromePath
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
// +build darwin freebsd netbsd openbsd
|
|
||||||
|
|
||||||
package runner
|
|
||||||
|
|
||||||
// ForceKill is a Chrome command line option that forces Chrome to be killed
|
|
||||||
// when the parent is killed.
|
|
||||||
//
|
|
||||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true (only for Linux)
|
|
||||||
func ForceKill(m map[string]interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
// +build linux
|
|
||||||
|
|
||||||
package runner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ByteCount is a type byte count const.
|
|
||||||
type ByteCount uint64
|
|
||||||
|
|
||||||
// ByteCount values.
|
|
||||||
const (
|
|
||||||
Byte ByteCount = 1
|
|
||||||
Kilobyte ByteCount = 1024 * Byte
|
|
||||||
Megabyte ByteCount = 1024 * Kilobyte
|
|
||||||
Gigabyte ByteCount = 1024 * Megabyte
|
|
||||||
)
|
|
||||||
|
|
||||||
// prlimit invokes the system's prlimit call. Copied from Go source tree.
|
|
||||||
//
|
|
||||||
// Note: this needs either the CAP_SYS_RESOURCE capability, or the invoking
|
|
||||||
// process needs to have the same functional user and group as the pid being
|
|
||||||
// modified.
|
|
||||||
//
|
|
||||||
// see: man 2 prlimit
|
|
||||||
func prlimit(pid int, res int, newv, old *syscall.Rlimit) error {
|
|
||||||
_, _, err := syscall.RawSyscall6(syscall.SYS_PRLIMIT64, uintptr(pid), uintptr(res), uintptr(unsafe.Pointer(newv)), uintptr(unsafe.Pointer(old)), 0, 0)
|
|
||||||
if err != 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rlimit is a Chrome command line option to set the soft rlimit value for res
|
|
||||||
// on a running Chrome process.
|
|
||||||
//
|
|
||||||
// Note: uses Linux prlimit system call, and is invoked after the child process
|
|
||||||
// has been started.
|
|
||||||
//
|
|
||||||
// see: man 2 prlimit
|
|
||||||
func Rlimit(res int, cur, max uint64) CommandLineOption {
|
|
||||||
return ProcessOpt(func(p *os.Process) error {
|
|
||||||
return prlimit(p.Pid, syscall.RLIMIT_AS, &syscall.Rlimit{
|
|
||||||
Cur: cur,
|
|
||||||
Max: max,
|
|
||||||
}, nil)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LimitMemory is a Chrome command line option to set the soft memory limit for
|
|
||||||
// a running Chrome process.
|
|
||||||
//
|
|
||||||
// Note: uses Linux prlimit system call, and is invoked after the child
|
|
||||||
// process has been started.
|
|
||||||
func LimitMemory(mem ByteCount) CommandLineOption {
|
|
||||||
return Rlimit(syscall.RLIMIT_AS, uint64(mem), uint64(mem))
|
|
||||||
}
|
|
||||||
|
|
||||||
// LimitCoreDump is a Chrome command line option to set the soft core dump
|
|
||||||
// limit for a running Chrome process.
|
|
||||||
//
|
|
||||||
// Note: uses Linux prlimit system call, and is invoked after the child
|
|
||||||
// process has been started.
|
|
||||||
func LimitCoreDump(sz ByteCount) CommandLineOption {
|
|
||||||
return Rlimit(syscall.RLIMIT_CORE, uint64(sz), uint64(sz))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForceKill is a Chrome command line option that forces Chrome to be killed
|
|
||||||
// when the parent is killed.
|
|
||||||
//
|
|
||||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true (only for Linux)
|
|
||||||
func ForceKill(m map[string]interface{}) error {
|
|
||||||
return CmdOpt(func(c *exec.Cmd) error {
|
|
||||||
if c.SysProcAttr == nil {
|
|
||||||
c.SysProcAttr = new(syscall.SysProcAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SysProcAttr.Pdeathsig = syscall.SIGKILL
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})(m)
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
// +build linux darwin freebsd netbsd openbsd
|
|
||||||
|
|
||||||
package runner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// DefaultUserDataTmpDir is the default directory path for created user
|
|
||||||
// data directories.
|
|
||||||
defaultUserDataTmpDir = "/tmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KillProcessGroup is a Chrome command line option that will instruct the
|
|
||||||
// invoked child Chrome process to terminate when the parent process (ie, the
|
|
||||||
// Go application) dies.
|
|
||||||
//
|
|
||||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true and does nothing on Windows.
|
|
||||||
func KillProcessGroup(m map[string]interface{}) error {
|
|
||||||
return CmdOpt(func(c *exec.Cmd) error {
|
|
||||||
if c.SysProcAttr == nil {
|
|
||||||
c.SysProcAttr = new(syscall.SysProcAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SysProcAttr.Setpgid = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})(m)
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
// +build windows
|
|
||||||
|
|
||||||
package runner
|
|
||||||
|
|
||||||
import "os"
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultUserDataTmpDir = os.Getenv("USERPROFILE") + `\AppData\Local`
|
|
||||||
)
|
|
||||||
|
|
||||||
// KillProcessGroup is a Chrome command line option that will instruct the
|
|
||||||
// invoked child Chrome process to terminate when the parent process (ie, the
|
|
||||||
// Go application) dies.
|
|
||||||
//
|
|
||||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true and does nothing on Windows.
|
|
||||||
func KillProcessGroup(m map[string]interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForceKill is a Chrome command line option that forces Chrome to be killed
|
|
||||||
// when the parent is killed.
|
|
||||||
//
|
|
||||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true (only for Linux)
|
|
||||||
func ForceKill(m map[string]interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
170
sel.go
170
sel.go
@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/cdp"
|
"github.com/chromedp/cdproto/cdp"
|
||||||
"github.com/chromedp/cdproto/dom"
|
"github.com/chromedp/cdproto/dom"
|
||||||
@ -26,9 +25,9 @@ tagname
|
|||||||
type Selector struct {
|
type Selector struct {
|
||||||
sel interface{}
|
sel interface{}
|
||||||
exp int
|
exp int
|
||||||
by func(context.Context, *TargetHandler, *cdp.Node) ([]cdp.NodeID, error)
|
by func(context.Context, *Target, *cdp.Node) ([]cdp.NodeID, error)
|
||||||
wait func(context.Context, *TargetHandler, *cdp.Node, ...cdp.NodeID) ([]*cdp.Node, error)
|
wait func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error)
|
||||||
after func(context.Context, *TargetHandler, ...*cdp.Node) error
|
after func(context.Context, *Target, ...*cdp.Node) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query is an action to query for document nodes match the specified sel and
|
// Query is an action to query for document nodes match the specified sel and
|
||||||
@ -56,21 +55,17 @@ func Query(sel interface{}, opts ...QueryOption) Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do satisfies the Action interface.
|
// Do satisfies the Action interface.
|
||||||
func (s *Selector) Do(ctxt context.Context, h cdp.Executor) error {
|
func (s *Selector) Do(ctx context.Context, h cdp.Executor) error {
|
||||||
th, ok := h.(*TargetHandler)
|
th, ok := h.(*Target)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrInvalidHandler
|
return ErrInvalidHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fix this
|
|
||||||
ctxt, cancel := context.WithTimeout(ctxt, 100*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
select {
|
select {
|
||||||
case err = <-s.run(ctxt, th):
|
case err = <-s.run(ctx, th):
|
||||||
case <-ctxt.Done():
|
case <-ctx.Done():
|
||||||
err = ctxt.Err()
|
err = ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
@ -79,54 +74,35 @@ func (s *Selector) Do(ctxt context.Context, h cdp.Executor) error {
|
|||||||
// run runs the selector action, starting over if the original returned nodes
|
// 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
|
// are invalidated prior to finishing the selector's by, wait, check, and after
|
||||||
// funcs.
|
// funcs.
|
||||||
func (s *Selector) run(ctxt context.Context, h *TargetHandler) chan error {
|
func (s *Selector) run(ctx context.Context, h *Target) chan error {
|
||||||
ch := make(chan error, 1)
|
ch := make(chan error, 1)
|
||||||
|
h.waitQueue <- func(cur *cdp.Frame) bool {
|
||||||
|
cur.RLock()
|
||||||
|
root := cur.Root
|
||||||
|
cur.RUnlock()
|
||||||
|
|
||||||
go func() {
|
if root == nil {
|
||||||
defer close(ch)
|
// not ready?
|
||||||
|
return false
|
||||||
for {
|
|
||||||
root, err := h.GetRoot(ctxt)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctxt.Done():
|
|
||||||
ch <- ctxt.Err()
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
ids, err := s.by(ctx, h, root)
|
||||||
default:
|
if err != nil || len(ids) < s.exp {
|
||||||
ids, err := s.by(ctxt, h, root)
|
return false
|
||||||
if err == nil && len(ids) >= s.exp {
|
|
||||||
nodes, err := s.wait(ctxt, h, root, ids...)
|
|
||||||
if err == nil {
|
|
||||||
if s.after == nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
nodes, err := s.wait(ctx, h, cur, ids...)
|
||||||
err = s.after(ctxt, h, nodes...)
|
// if nodes==nil, we're not yet ready
|
||||||
if err != nil {
|
if nodes == nil || err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.after != nil {
|
||||||
|
if err := s.after(ctx, h, nodes...); err != nil {
|
||||||
ch <- err
|
ch <- err
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
close(ch)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(DefaultCheckDuration)
|
|
||||||
|
|
||||||
case <-root.Invalidated:
|
|
||||||
continue
|
|
||||||
|
|
||||||
case <-ctxt.Done():
|
|
||||||
ch <- ctxt.Err()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,20 +115,10 @@ func (s *Selector) selAsString() string {
|
|||||||
return fmt.Sprintf("%s", s.sel)
|
return fmt.Sprintf("%s", s.sel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// selAsInt forces sel into a int.
|
|
||||||
/*func (s *Selector) selAsInt() int {
|
|
||||||
sel, ok := s.sel.(int)
|
|
||||||
if !ok {
|
|
||||||
panic("selector must be int")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sel
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// QueryAfter is an action that will match the specified sel using the supplied
|
// 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
|
// query options, and after the visibility conditions of the query have been
|
||||||
// met, will execute f.
|
// met, will execute f.
|
||||||
func QueryAfter(sel interface{}, f func(context.Context, *TargetHandler, ...*cdp.Node) error, opts ...QueryOption) Action {
|
func QueryAfter(sel interface{}, f func(context.Context, *Target, ...*cdp.Node) error, opts ...QueryOption) Action {
|
||||||
return Query(sel, append(opts, After(f))...)
|
return Query(sel, append(opts, After(f))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +126,7 @@ func QueryAfter(sel interface{}, f func(context.Context, *TargetHandler, ...*cdp
|
|||||||
type QueryOption func(*Selector)
|
type QueryOption func(*Selector)
|
||||||
|
|
||||||
// ByFunc is a query option to set the func used to select elements.
|
// ByFunc is a query option to set the func used to select elements.
|
||||||
func ByFunc(f func(context.Context, *TargetHandler, *cdp.Node) ([]cdp.NodeID, error)) QueryOption {
|
func ByFunc(f func(context.Context, *Target, *cdp.Node) ([]cdp.NodeID, error)) QueryOption {
|
||||||
return func(s *Selector) {
|
return func(s *Selector) {
|
||||||
s.by = f
|
s.by = f
|
||||||
}
|
}
|
||||||
@ -169,8 +135,8 @@ func ByFunc(f func(context.Context, *TargetHandler, *cdp.Node) ([]cdp.NodeID, er
|
|||||||
// ByQuery is a query option to select a single element using
|
// ByQuery is a query option to select a single element using
|
||||||
// DOM.querySelector.
|
// DOM.querySelector.
|
||||||
func ByQuery(s *Selector) {
|
func ByQuery(s *Selector) {
|
||||||
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
ByFunc(func(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||||
nodeID, err := dom.QuerySelector(n.NodeID, s.selAsString()).Do(ctxt, h)
|
nodeID, err := dom.QuerySelector(n.NodeID, s.selAsString()).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -185,8 +151,8 @@ func ByQuery(s *Selector) {
|
|||||||
|
|
||||||
// ByQueryAll is a query option to select elements by DOM.querySelectorAll.
|
// ByQueryAll is a query option to select elements by DOM.querySelectorAll.
|
||||||
func ByQueryAll(s *Selector) {
|
func ByQueryAll(s *Selector) {
|
||||||
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
ByFunc(func(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||||
return dom.QuerySelectorAll(n.NodeID, s.selAsString()).Do(ctxt, h)
|
return dom.QuerySelectorAll(n.NodeID, s.selAsString()).Do(ctx, h)
|
||||||
})(s)
|
})(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,8 +165,8 @@ func ByID(s *Selector) {
|
|||||||
// BySearch is a query option via DOM.performSearch (works with both CSS and
|
// BySearch is a query option via DOM.performSearch (works with both CSS and
|
||||||
// XPath queries).
|
// XPath queries).
|
||||||
func BySearch(s *Selector) {
|
func BySearch(s *Selector) {
|
||||||
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
ByFunc(func(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||||
id, count, err := dom.PerformSearch(s.selAsString()).Do(ctxt, h)
|
id, count, err := dom.PerformSearch(s.selAsString()).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -209,7 +175,7 @@ func BySearch(s *Selector) {
|
|||||||
return []cdp.NodeID{}, nil
|
return []cdp.NodeID{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes, err := dom.GetSearchResults(id, 0, count).Do(ctxt, h)
|
nodes, err := dom.GetSearchResults(id, 0, count).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -225,9 +191,9 @@ func ByNodeID(s *Selector) {
|
|||||||
panic("ByNodeID can only work on []cdp.NodeID")
|
panic("ByNodeID can only work on []cdp.NodeID")
|
||||||
}
|
}
|
||||||
|
|
||||||
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
ByFunc(func(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
err := dom.RequestChildNodes(id).WithPierce(true).Do(ctxt, h)
|
err := dom.RequestChildNodes(id).WithPierce(true).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -238,38 +204,28 @@ func ByNodeID(s *Selector) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// waitReady waits for the specified nodes to be ready.
|
// waitReady waits for the specified nodes to be ready.
|
||||||
func (s *Selector) waitReady(check func(context.Context, *TargetHandler, *cdp.Node) error) func(context.Context, *TargetHandler, *cdp.Node, ...cdp.NodeID) ([]*cdp.Node, error) {
|
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 *TargetHandler, n *cdp.Node, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
return func(ctx context.Context, h *Target, cur *cdp.Frame, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
||||||
f, err := h.WaitFrame(ctxt, cdp.EmptyFrameID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := new(sync.WaitGroup)
|
|
||||||
nodes := make([]*cdp.Node, len(ids))
|
nodes := make([]*cdp.Node, len(ids))
|
||||||
errs := make([]error, len(ids))
|
cur.RLock()
|
||||||
for i, id := range ids {
|
for i, id := range ids {
|
||||||
wg.Add(1)
|
nodes[i] = cur.Nodes[id]
|
||||||
go func(i int, id cdp.NodeID) {
|
if nodes[i] == nil {
|
||||||
defer wg.Done()
|
cur.RUnlock()
|
||||||
nodes[i], errs[i] = h.WaitNode(ctxt, f, id)
|
// not yet ready
|
||||||
}(i, id)
|
return nil, nil
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
for _, err := range errs {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cur.RUnlock()
|
||||||
|
|
||||||
if check != nil {
|
if check != nil {
|
||||||
|
var wg sync.WaitGroup
|
||||||
errs := make([]error, len(nodes))
|
errs := make([]error, len(nodes))
|
||||||
for i, n := range nodes {
|
for i, n := range nodes {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(i int, n *cdp.Node) {
|
go func(i int, n *cdp.Node) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
errs[i] = check(ctxt, h, n)
|
errs[i] = check(ctx, h, n)
|
||||||
}(i, n)
|
}(i, n)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@ -286,7 +242,7 @@ func (s *Selector) waitReady(check func(context.Context, *TargetHandler, *cdp.No
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WaitFunc is a query option to set a custom wait func.
|
// WaitFunc is a query option to set a custom wait func.
|
||||||
func WaitFunc(wait func(context.Context, *TargetHandler, *cdp.Node, ...cdp.NodeID) ([]*cdp.Node, error)) QueryOption {
|
func WaitFunc(wait func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error)) QueryOption {
|
||||||
return func(s *Selector) {
|
return func(s *Selector) {
|
||||||
s.wait = wait
|
s.wait = wait
|
||||||
}
|
}
|
||||||
@ -299,9 +255,9 @@ func NodeReady(s *Selector) {
|
|||||||
|
|
||||||
// NodeVisible is a query option to wait until the element is visible.
|
// NodeVisible is a query option to wait until the element is visible.
|
||||||
func NodeVisible(s *Selector) {
|
func NodeVisible(s *Selector) {
|
||||||
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
WaitFunc(s.waitReady(func(ctx context.Context, h *Target, n *cdp.Node) error {
|
||||||
// check box model
|
// check box model
|
||||||
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isCouldNotComputeBoxModelError(err) {
|
if isCouldNotComputeBoxModelError(err) {
|
||||||
return ErrNotVisible
|
return ErrNotVisible
|
||||||
@ -312,7 +268,7 @@ func NodeVisible(s *Selector) {
|
|||||||
|
|
||||||
// check offsetParent
|
// 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(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -325,9 +281,9 @@ func NodeVisible(s *Selector) {
|
|||||||
|
|
||||||
// NodeNotVisible 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 NodeNotVisible(s *Selector) {
|
func NodeNotVisible(s *Selector) {
|
||||||
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
WaitFunc(s.waitReady(func(ctx context.Context, h *Target, n *cdp.Node) error {
|
||||||
// check box model
|
// check box model
|
||||||
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isCouldNotComputeBoxModelError(err) {
|
if isCouldNotComputeBoxModelError(err) {
|
||||||
return nil
|
return nil
|
||||||
@ -338,7 +294,7 @@ func NodeNotVisible(s *Selector) {
|
|||||||
|
|
||||||
// check offsetParent
|
// 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(ctx, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -351,7 +307,7 @@ func NodeNotVisible(s *Selector) {
|
|||||||
|
|
||||||
// NodeEnabled is a query option to wait until the element is enabled.
|
// NodeEnabled is a query option to wait until the element is enabled.
|
||||||
func NodeEnabled(s *Selector) {
|
func NodeEnabled(s *Selector) {
|
||||||
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
WaitFunc(s.waitReady(func(ctx context.Context, h *Target, n *cdp.Node) error {
|
||||||
n.RLock()
|
n.RLock()
|
||||||
defer n.RUnlock()
|
defer n.RUnlock()
|
||||||
|
|
||||||
@ -367,7 +323,7 @@ func NodeEnabled(s *Selector) {
|
|||||||
|
|
||||||
// NodeSelected is a query option to wait until the element is selected.
|
// NodeSelected is a query option to wait until the element is selected.
|
||||||
func NodeSelected(s *Selector) {
|
func NodeSelected(s *Selector) {
|
||||||
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
WaitFunc(s.waitReady(func(ctx context.Context, h *Target, n *cdp.Node) error {
|
||||||
n.RLock()
|
n.RLock()
|
||||||
defer n.RUnlock()
|
defer n.RUnlock()
|
||||||
|
|
||||||
@ -381,11 +337,11 @@ func NodeSelected(s *Selector) {
|
|||||||
}))(s)
|
}))(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeNotPresent is a query option to wait until no elements match are
|
// NodeNotPresent is a query option to wait until no elements are present
|
||||||
// present matching the selector.
|
// matching the selector.
|
||||||
func NodeNotPresent(s *Selector) {
|
func NodeNotPresent(s *Selector) {
|
||||||
s.exp = 0
|
s.exp = 0
|
||||||
WaitFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
WaitFunc(func(ctx context.Context, h *Target, cur *cdp.Frame, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
||||||
if len(ids) != 0 {
|
if len(ids) != 0 {
|
||||||
return nil, ErrHasResults
|
return nil, ErrHasResults
|
||||||
}
|
}
|
||||||
@ -403,7 +359,7 @@ func AtLeast(n int) QueryOption {
|
|||||||
|
|
||||||
// After is a query option to set a func that will be executed after the wait
|
// After is a query option to set a func that will be executed after the wait
|
||||||
// has succeeded.
|
// has succeeded.
|
||||||
func After(f func(context.Context, *TargetHandler, ...*cdp.Node) error) QueryOption {
|
func After(f func(context.Context, *Target, ...*cdp.Node) error) QueryOption {
|
||||||
return func(s *Selector) {
|
return func(s *Selector) {
|
||||||
s.after = f
|
s.after = f
|
||||||
}
|
}
|
||||||
|
163
sel_test.go
163
sel_test.go
@ -9,26 +9,21 @@ import (
|
|||||||
func TestWaitReady(t *testing.T) {
|
func TestWaitReady(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var nodeIDs []cdp.NodeID
|
var nodeIDs []cdp.NodeID
|
||||||
err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID))
|
if err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if len(nodeIDs) != 1 {
|
if len(nodeIDs) != 1 {
|
||||||
t.Errorf("expected to have exactly 1 node id: got %d", len(nodeIDs))
|
t.Errorf("expected to have exactly 1 node id: got %d", len(nodeIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitReady("#input2", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
WaitReady("#input2", ByID),
|
||||||
|
Value(nodeIDs, &value, ByNodeID),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,26 +31,21 @@ func TestWaitReady(t *testing.T) {
|
|||||||
func TestWaitVisible(t *testing.T) {
|
func TestWaitVisible(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var nodeIDs []cdp.NodeID
|
var nodeIDs []cdp.NodeID
|
||||||
err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID))
|
if err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if len(nodeIDs) != 1 {
|
if len(nodeIDs) != 1 {
|
||||||
t.Errorf("expected to have exactly 1 node id: got %d", len(nodeIDs))
|
t.Errorf("expected to have exactly 1 node id: got %d", len(nodeIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitVisible("#input2", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
WaitVisible("#input2", ByID),
|
||||||
|
Value(nodeIDs, &value, ByNodeID),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,31 +53,22 @@ func TestWaitVisible(t *testing.T) {
|
|||||||
func TestWaitNotVisible(t *testing.T) {
|
func TestWaitNotVisible(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var nodeIDs []cdp.NodeID
|
var nodeIDs []cdp.NodeID
|
||||||
err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID))
|
if err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if len(nodeIDs) != 1 {
|
if len(nodeIDs) != 1 {
|
||||||
t.Errorf("expected to have exactly 1 node id: got %d", len(nodeIDs))
|
t.Errorf("expected to have exactly 1 node id: got %d", len(nodeIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, Click("#button2", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitNotVisible("#input2", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
Click("#button2", ByID),
|
||||||
|
WaitNotVisible("#input2", ByID),
|
||||||
|
Value(nodeIDs, &value, ByNodeID),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,46 +76,35 @@ func TestWaitNotVisible(t *testing.T) {
|
|||||||
func TestWaitEnabled(t *testing.T) {
|
func TestWaitEnabled(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var attr string
|
var attr string
|
||||||
var ok bool
|
var ok bool
|
||||||
err := c.Run(defaultContext, AttributeValue("#select1", "disabled", &attr, &ok, ByID))
|
if err := Run(ctx, AttributeValue("#select1", "disabled", &attr, &ok, ByID)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected element to be disabled")
|
t.Fatal("expected element to be disabled")
|
||||||
}
|
}
|
||||||
|
if err := Run(ctx,
|
||||||
err = c.Run(defaultContext, Click("#button3", ByID))
|
Click("#button3", ByID),
|
||||||
if err != nil {
|
WaitEnabled("#select1", ByID),
|
||||||
t.Fatalf("got error: %v", err)
|
AttributeValue("#select1", "disabled", &attr, &ok, ByID),
|
||||||
}
|
); err != nil {
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitEnabled("#select1", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
err = c.Run(defaultContext, AttributeValue("#select1", "disabled", &attr, &ok, ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
t.Fatal("expected element to be enabled")
|
t.Fatal("expected element to be enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
err = c.Run(defaultContext, Value("#select1", &value, ByID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"),
|
||||||
|
Value("#select1", &value, ByID),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != "foo" {
|
if value != "foo" {
|
||||||
t.Fatalf("expected value to be foo, got: %s", value)
|
t.Fatalf("expected value to be foo, got: %s", value)
|
||||||
}
|
}
|
||||||
@ -143,43 +113,32 @@ func TestWaitEnabled(t *testing.T) {
|
|||||||
func TestWaitSelected(t *testing.T) {
|
func TestWaitSelected(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, Click("#button3", ByID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
Click("#button3", ByID),
|
||||||
t.Fatalf("got error: %v", err)
|
WaitEnabled("#select1", ByID),
|
||||||
}
|
); err != nil {
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitEnabled("#select1", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var attr string
|
var attr string
|
||||||
ok := false
|
ok := false
|
||||||
err = c.Run(defaultContext, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, &ok))
|
if err := Run(ctx, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, &ok)); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
t.Fatal("expected element to be not selected")
|
t.Fatal("expected element to be not selected")
|
||||||
}
|
}
|
||||||
|
if err := Run(ctx,
|
||||||
err = c.Run(defaultContext, SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"))
|
SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"),
|
||||||
if err != nil {
|
WaitSelected(`//*[@id="select1"]/option[1]`),
|
||||||
|
AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, nil),
|
||||||
|
); err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitSelected(`//*[@id="select1"]/option[1]`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, nil))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
if attr != "true" {
|
if attr != "true" {
|
||||||
t.Fatal("expected element to be selected")
|
t.Fatal("expected element to be selected")
|
||||||
}
|
}
|
||||||
@ -188,21 +147,14 @@ func TestWaitSelected(t *testing.T) {
|
|||||||
func TestWaitNotPresent(t *testing.T) {
|
func TestWaitNotPresent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
err := c.Run(defaultContext, WaitVisible("#input3", ByID))
|
if err := Run(ctx,
|
||||||
if err != nil {
|
WaitVisible("#input3", ByID),
|
||||||
t.Fatalf("got error: %v", err)
|
Click("#button4", ByID),
|
||||||
}
|
WaitNotPresent("#input3", ByID),
|
||||||
|
); err != nil {
|
||||||
err = c.Run(defaultContext, Click("#button4", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Run(defaultContext, WaitNotPresent("#input3", ByID))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,12 +162,11 @@ func TestWaitNotPresent(t *testing.T) {
|
|||||||
func TestAtLeast(t *testing.T) {
|
func TestAtLeast(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testAllocate(t, "js.html")
|
ctx, cancel := testAllocate(t, "js.html")
|
||||||
defer c.Release()
|
defer cancel()
|
||||||
|
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
err := c.Run(defaultContext, Nodes("//input", &nodes, AtLeast(3)))
|
if err := Run(ctx, Nodes("//input", &nodes, AtLeast(3))); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if len(nodes) < 3 {
|
if len(nodes) < 3 {
|
||||||
|
320
target.go
Normal file
320
target.go
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
package chromedp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mailru/easyjson"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto"
|
||||||
|
"github.com/chromedp/cdproto/cdp"
|
||||||
|
"github.com/chromedp/cdproto/dom"
|
||||||
|
"github.com/chromedp/cdproto/inspector"
|
||||||
|
"github.com/chromedp/cdproto/page"
|
||||||
|
"github.com/chromedp/cdproto/target"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Target manages a Chrome DevTools Protocol target.
|
||||||
|
type Target struct {
|
||||||
|
browser *Browser
|
||||||
|
SessionID target.SessionID
|
||||||
|
TargetID target.ID
|
||||||
|
|
||||||
|
waitQueue chan func(cur *cdp.Frame) bool
|
||||||
|
eventQueue chan *cdproto.Message
|
||||||
|
|
||||||
|
// below are the old TargetHandler fields.
|
||||||
|
|
||||||
|
// frames is the set of encountered frames.
|
||||||
|
frames map[cdp.FrameID]*cdp.Frame
|
||||||
|
|
||||||
|
// cur is the current top level frame.
|
||||||
|
cur *cdp.Frame
|
||||||
|
|
||||||
|
// logging funcs
|
||||||
|
logf, errf func(string, ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Target) run(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case msg := <-t.eventQueue:
|
||||||
|
if err := t.processEvent(ctx, msg); err != nil {
|
||||||
|
t.errf("could not process event: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// prevent busy spinning. TODO: do better
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
n := len(t.waitQueue)
|
||||||
|
if n == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t.cur == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
fn := <-t.waitQueue
|
||||||
|
if !fn(t.cur) {
|
||||||
|
// try again later.
|
||||||
|
t.waitQueue <- fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Target) Execute(ctx context.Context, method string, params json.Marshaler, res json.Unmarshaler) error {
|
||||||
|
paramsMsg := emptyObj
|
||||||
|
if params != nil {
|
||||||
|
var err error
|
||||||
|
if paramsMsg, err = json.Marshal(params); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
innerID := atomic.AddInt64(&t.browser.next, 1)
|
||||||
|
msg := &cdproto.Message{
|
||||||
|
ID: innerID,
|
||||||
|
Method: cdproto.MethodType(method),
|
||||||
|
Params: paramsMsg,
|
||||||
|
}
|
||||||
|
msgJSON, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sendParams := target.SendMessageToTarget(string(msgJSON)).
|
||||||
|
WithSessionID(t.SessionID)
|
||||||
|
sendParamsJSON, _ := json.Marshal(sendParams)
|
||||||
|
|
||||||
|
// We want to grab the response from the inner message.
|
||||||
|
ch := make(chan *cdproto.Message, 1)
|
||||||
|
t.browser.cmdQueue <- cmdJob{
|
||||||
|
msg: &cdproto.Message{ID: innerID},
|
||||||
|
resp: ch,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The response from the outer message is uninteresting; pass a nil
|
||||||
|
// resp channel.
|
||||||
|
outerID := atomic.AddInt64(&t.browser.next, 1)
|
||||||
|
t.browser.cmdQueue <- cmdJob{
|
||||||
|
msg: &cdproto.Message{
|
||||||
|
ID: outerID,
|
||||||
|
Method: target.CommandSendMessageToTarget,
|
||||||
|
Params: sendParamsJSON,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case msg := <-ch:
|
||||||
|
switch {
|
||||||
|
case msg == nil:
|
||||||
|
return ErrChannelClosed
|
||||||
|
case msg.Error != nil:
|
||||||
|
return msg.Error
|
||||||
|
case res != nil:
|
||||||
|
return json.Unmarshal(msg.Result, res)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// below are the old TargetHandler methods.
|
||||||
|
|
||||||
|
// processEvent processes an incoming event.
|
||||||
|
func (t *Target) processEvent(ctx context.Context, msg *cdproto.Message) error {
|
||||||
|
if msg == nil {
|
||||||
|
return ErrChannelClosed
|
||||||
|
}
|
||||||
|
// unmarshal
|
||||||
|
ev, err := cdproto.UnmarshalMessage(msg)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "unknown command or event") {
|
||||||
|
// This is most likely an event received from an older
|
||||||
|
// Chrome which a newer cdproto doesn't have, as it is
|
||||||
|
// deprecated. Ignore that error.
|
||||||
|
// TODO: use error wrapping once Go 1.13 is released.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ev.(type) {
|
||||||
|
case *inspector.EventDetached:
|
||||||
|
return nil
|
||||||
|
case *dom.EventDocumentUpdated:
|
||||||
|
t.documentUpdated(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.Method.Domain() {
|
||||||
|
case "Page":
|
||||||
|
t.pageEvent(ev)
|
||||||
|
case "DOM":
|
||||||
|
t.domEvent(ev)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// documentUpdated handles the document updated event, retrieving the document
|
||||||
|
// root for the root frame.
|
||||||
|
func (t *Target) documentUpdated(ctx context.Context) {
|
||||||
|
f := t.cur
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
|
||||||
|
// invalidate nodes
|
||||||
|
if f.Root != nil {
|
||||||
|
close(f.Root.Invalidated)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Nodes = make(map[cdp.NodeID]*cdp.Node)
|
||||||
|
var err error
|
||||||
|
f.Root, err = dom.GetDocument().WithPierce(true).Do(ctx, t)
|
||||||
|
if err == context.Canceled {
|
||||||
|
return // TODO: perhaps not necessary, but useful to keep the tests less noisy
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.errf("could not retrieve document root for %s: %v", f.ID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.Root.Invalidated = make(chan struct{})
|
||||||
|
walk(f.Nodes, f.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emptyObj is an empty JSON object message.
|
||||||
|
var emptyObj = easyjson.RawMessage([]byte(`{}`))
|
||||||
|
|
||||||
|
// pageEvent handles incoming page events.
|
||||||
|
func (t *Target) pageEvent(ev interface{}) {
|
||||||
|
var id cdp.FrameID
|
||||||
|
var op frameOp
|
||||||
|
|
||||||
|
switch e := ev.(type) {
|
||||||
|
case *page.EventFrameNavigated:
|
||||||
|
t.frames[e.Frame.ID] = e.Frame
|
||||||
|
t.cur = e.Frame
|
||||||
|
return
|
||||||
|
|
||||||
|
case *page.EventFrameAttached:
|
||||||
|
id, op = e.FrameID, frameAttached(e.ParentFrameID)
|
||||||
|
|
||||||
|
case *page.EventFrameDetached:
|
||||||
|
id, op = e.FrameID, frameDetached
|
||||||
|
|
||||||
|
case *page.EventFrameStartedLoading:
|
||||||
|
id, op = e.FrameID, frameStartedLoading
|
||||||
|
|
||||||
|
case *page.EventFrameStoppedLoading:
|
||||||
|
id, op = e.FrameID, frameStoppedLoading
|
||||||
|
|
||||||
|
// ignored events
|
||||||
|
case *page.EventFrameRequestedNavigation:
|
||||||
|
return
|
||||||
|
case *page.EventDomContentEventFired:
|
||||||
|
return
|
||||||
|
case *page.EventLoadEventFired:
|
||||||
|
return
|
||||||
|
case *page.EventFrameResized:
|
||||||
|
return
|
||||||
|
case *page.EventLifecycleEvent:
|
||||||
|
return
|
||||||
|
case *page.EventNavigatedWithinDocument:
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.errf("unhandled page event %T", ev)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f := t.frames[id]
|
||||||
|
if f == nil {
|
||||||
|
// This can happen if a frame is attached or starts loading
|
||||||
|
// before it's ever navigated to. We won't have all the frame
|
||||||
|
// details just yet, but that's okay.
|
||||||
|
f = &cdp.Frame{ID: id}
|
||||||
|
t.frames[id] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
|
||||||
|
op(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// domEvent handles incoming DOM events.
|
||||||
|
func (t *Target) domEvent(ev interface{}) {
|
||||||
|
f := t.cur
|
||||||
|
|
||||||
|
var id cdp.NodeID
|
||||||
|
var op nodeOp
|
||||||
|
|
||||||
|
switch e := ev.(type) {
|
||||||
|
case *dom.EventSetChildNodes:
|
||||||
|
id, op = e.ParentID, setChildNodes(f.Nodes, e.Nodes)
|
||||||
|
|
||||||
|
case *dom.EventAttributeModified:
|
||||||
|
id, op = e.NodeID, attributeModified(e.Name, e.Value)
|
||||||
|
|
||||||
|
case *dom.EventAttributeRemoved:
|
||||||
|
id, op = e.NodeID, attributeRemoved(e.Name)
|
||||||
|
|
||||||
|
case *dom.EventInlineStyleInvalidated:
|
||||||
|
if len(e.NodeIds) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, op = e.NodeIds[0], inlineStyleInvalidated(e.NodeIds[1:])
|
||||||
|
|
||||||
|
case *dom.EventCharacterDataModified:
|
||||||
|
id, op = e.NodeID, characterDataModified(e.CharacterData)
|
||||||
|
|
||||||
|
case *dom.EventChildNodeCountUpdated:
|
||||||
|
id, op = e.NodeID, childNodeCountUpdated(e.ChildNodeCount)
|
||||||
|
|
||||||
|
case *dom.EventChildNodeInserted:
|
||||||
|
id, op = e.ParentNodeID, childNodeInserted(f.Nodes, e.PreviousNodeID, e.Node)
|
||||||
|
|
||||||
|
case *dom.EventChildNodeRemoved:
|
||||||
|
id, op = e.ParentNodeID, childNodeRemoved(f.Nodes, e.NodeID)
|
||||||
|
|
||||||
|
case *dom.EventShadowRootPushed:
|
||||||
|
id, op = e.HostID, shadowRootPushed(f.Nodes, e.Root)
|
||||||
|
|
||||||
|
case *dom.EventShadowRootPopped:
|
||||||
|
id, op = e.HostID, shadowRootPopped(f.Nodes, e.RootID)
|
||||||
|
|
||||||
|
case *dom.EventPseudoElementAdded:
|
||||||
|
id, op = e.ParentID, pseudoElementAdded(f.Nodes, e.PseudoElement)
|
||||||
|
|
||||||
|
case *dom.EventPseudoElementRemoved:
|
||||||
|
id, op = e.ParentID, pseudoElementRemoved(f.Nodes, e.PseudoElementID)
|
||||||
|
|
||||||
|
case *dom.EventDistributedNodesUpdated:
|
||||||
|
id, op = e.InsertionPointID, distributedNodesUpdated(e.DistributedNodes)
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.errf("unhandled node event %T", ev)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, ok := f.Nodes[id]
|
||||||
|
if !ok {
|
||||||
|
// Node ID has been invalidated. Nothing to do.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
|
||||||
|
op(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TargetOption func(*Target)
|
9
testdata/iframe.html
vendored
Normal file
9
testdata/iframe.html
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>page with an iframe</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<iframe src="form.html"></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user