Compare commits
No commits in common. "master" and "v0.1.3" have entirely different histories.
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 git.loafle.net/commons_go/chromedp
|
$ go list -m github.com/chromedp/chromedp
|
||||||
$ chromium --version
|
$ chromium --version
|
||||||
$ go version
|
$ go version
|
||||||
</pre>
|
</pre>
|
||||||
|
13
.travis.yml
13
.travis.yml
@ -1,11 +1,14 @@
|
|||||||
language: go
|
language: go
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.12.x
|
- 1.10.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:
|
||||||
- go test -v ./...
|
- export CHROMEDP_TEST_RUNNER=google-chrome-stable
|
||||||
|
- 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,14 +9,13 @@ 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 git.loafle.net/commons_go/chromedp
|
go get -u github.com/chromedp/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, which also contains
|
[GoDoc API listing][7] for a summary of the API and Actions.
|
||||||
a few simple and runnable examples.
|
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
@ -25,10 +24,11 @@ a few simple and runnable examples.
|
|||||||
* [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`
|
||||||
* [`git.loafle.net/commons_go/chromedp-proxy`][11] - a simple CDP proxy for logging CDP clients and browsers
|
* [`github.com/chromedp/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 @@ a few simple and runnable examples.
|
|||||||
[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/git.loafle.net/commons_go/chromedp
|
[7]: https://godoc.org/github.com/chromedp/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://git.loafle.net/commons_go/chromedp-proxy
|
[11]: https://github.com/chromedp/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(ctx context.Context, h cdp.Executor) error {
|
func (f ActionFunc) Do(ctxt context.Context, h cdp.Executor) error {
|
||||||
return f(ctx, h)
|
return f(ctxt, 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(ctx context.Context, h cdp.Executor) error {
|
func (t Tasks) Do(ctxt 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 {
|
||||||
// ctx, cancel = context.WithTimeout(ctx, timeout)
|
// ctxt, cancel = context.WithTimeout(ctxt, timeout)
|
||||||
// defer cancel()
|
// defer cancel()
|
||||||
if err := a.Do(ctx, h); err != nil {
|
if err := a.Do(ctxt, h); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,15 +46,12 @@ func (t Tasks) Do(ctx 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(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt 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 <-t.C:
|
case <-time.After(d):
|
||||||
case <-ctx.Done():
|
|
||||||
t.Stop()
|
case <-ctxt.Done():
|
||||||
return ctx.Err()
|
return ctxt.Err()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
288
allocate.go
288
allocate.go
@ -1,288 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
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
321
browser.go
@ -1,321 +0,0 @@
|
|||||||
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) {
|
|
||||||
}
|
|
||||||
}
|
|
653
chromedp.go
653
chromedp.go
@ -8,285 +8,428 @@ package chromedp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/css"
|
"github.com/chromedp/cdproto/cdp"
|
||||||
"github.com/chromedp/cdproto/dom"
|
|
||||||
"github.com/chromedp/cdproto/inspector"
|
"github.com/chromedp/chromedp/client"
|
||||||
"github.com/chromedp/cdproto/log"
|
"github.com/chromedp/chromedp/runner"
|
||||||
"github.com/chromedp/cdproto/page"
|
|
||||||
"github.com/chromedp/cdproto/runtime"
|
|
||||||
"github.com/chromedp/cdproto/target"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context is attached to any context.Context which is valid for use with Run.
|
const (
|
||||||
type Context struct {
|
// DefaultNewTargetTimeout is the default time to wait for a new target to
|
||||||
// Allocator is used to create new browsers. It is inherited from the
|
// be started.
|
||||||
// parent context when using NewContext.
|
DefaultNewTargetTimeout = 3 * time.Second
|
||||||
Allocator Allocator
|
|
||||||
|
|
||||||
// Browser is the browser being used in the context. It is inherited
|
// DefaultCheckDuration is the default time to sleep between a check.
|
||||||
// from the parent context when using NewContext.
|
DefaultCheckDuration = 50 * time.Millisecond
|
||||||
Browser *Browser
|
|
||||||
|
|
||||||
// Target is the target to run actions (commands) against. It is not
|
// DefaultPoolStartPort is the default start port number.
|
||||||
// inherited from the parent context, and typically each context will
|
DefaultPoolStartPort = 9000
|
||||||
// have its own unique Target pointing to a separate browser tab (page).
|
|
||||||
Target *Target
|
|
||||||
|
|
||||||
// browserOpts holds the browser options passed to NewContext via
|
// DefaultPoolEndPort is the default end port number.
|
||||||
// WithBrowserOption, so that they can later be used when allocating a
|
DefaultPoolEndPort = 10000
|
||||||
// browser in Run.
|
)
|
||||||
browserOpts []BrowserOption
|
|
||||||
|
|
||||||
// cancel simply cancels the context that was used to start Browser.
|
// CDP is the high-level Chrome DevTools Protocol browser manager, handling the
|
||||||
// This is useful to stop all activity and avoid deadlocks if we detect
|
// browser process runner, WebSocket clients, associated targets, and network,
|
||||||
// that the browser was closed or happened to crash. Note that this
|
// page, and DOM events.
|
||||||
// cancel function doesn't do any waiting.
|
type CDP struct {
|
||||||
cancel func()
|
// r is the chrome runner.
|
||||||
|
r *runner.Runner
|
||||||
|
|
||||||
// first records whether this context was the one that allocated
|
// opts are command line options to pass to a created runner.
|
||||||
// Browser. This is important, because its cancellation will stop the
|
opts []runner.CommandLineOption
|
||||||
// entire browser handler, meaning that no further actions can be
|
|
||||||
// executed.
|
|
||||||
first bool
|
|
||||||
|
|
||||||
// wg allows waiting for a target to be closed on cancellation.
|
// watch is the channel for new client targets.
|
||||||
wg sync.WaitGroup
|
watch <-chan client.Target
|
||||||
|
|
||||||
// cancelErr is the first error encountered when cancelling this
|
// cur is the current active target's handler.
|
||||||
// context, for example if a browser's temporary user data directory
|
cur cdp.Executor
|
||||||
// couldn't be deleted.
|
|
||||||
cancelErr error
|
// handlers is the active handlers.
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContext creates a chromedp context from the parent context. The parent
|
// New creates and starts a new CDP instance.
|
||||||
// context's Allocator is inherited, defaulting to an ExecAllocator with
|
func New(ctxt context.Context, opts ...Option) (*CDP, error) {
|
||||||
// DefaultExecAllocatorOptions.
|
c := &CDP{
|
||||||
//
|
handlers: make([]*TargetHandler, 0),
|
||||||
// If the parent context contains an allocated Browser, the child context
|
handlerMap: make(map[string]int),
|
||||||
// inherits it, and its first Run creates a new tab on that browser. Otherwise,
|
logf: log.Printf,
|
||||||
// its first Run will allocate a new browser.
|
debugf: func(string, ...interface{}) {},
|
||||||
//
|
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 {
|
||||||
o(c)
|
if err := o(c); err != nil {
|
||||||
}
|
return nil, err
|
||||||
if c.Allocator == nil {
|
|
||||||
c.Allocator = setupExecAllocator(DefaultExecAllocatorOptions...)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, contextKey{}, c)
|
|
||||||
c.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
if c.first {
|
|
||||||
// This is the original browser tab, so the entire
|
|
||||||
// browser will already be cleaned up elsewhere.
|
|
||||||
c.wg.Done()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
cancel()
|
|
||||||
c.wg.Wait()
|
|
||||||
}
|
|
||||||
return ctx, cancelWait
|
|
||||||
}
|
|
||||||
|
|
||||||
type contextKey struct{}
|
|
||||||
|
|
||||||
// FromContext extracts the Context data stored inside a context.Context.
|
|
||||||
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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Browser = browser
|
|
||||||
}
|
|
||||||
if c.Target == nil {
|
|
||||||
if err := c.newSession(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Tasks(actions).Do(ctx, c.Target)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) newSession(ctx context.Context) error {
|
|
||||||
var targetID target.ID
|
|
||||||
if c.first {
|
|
||||||
// If we just allocated this browser, and it has a single page
|
|
||||||
// that's blank and not attached, use it.
|
|
||||||
infos, err := target.GetTargets().Do(ctx, c.Browser)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pages := 0
|
|
||||||
for _, info := range infos {
|
|
||||||
if info.Type == "page" && info.URL == "about:blank" && !info.Attached {
|
|
||||||
targetID = info.TargetID
|
|
||||||
pages++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pages > 1 {
|
|
||||||
// Multiple blank pages; just in case, don't use any.
|
|
||||||
targetID = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetID == "" {
|
// check for supplied runner, if none then create one
|
||||||
|
if c.r == nil && c.watch == nil {
|
||||||
var err error
|
var err error
|
||||||
targetID, err = target.CreateTarget("about:blank").Do(ctx, c.Browser)
|
c.r, err = runner.Run(ctxt, c.opts...)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID, err := target.AttachToTarget(targetID).Do(ctx, c.Browser)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Target = c.Browser.newExecutorForTarget(ctx, targetID, sessionID)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContextOption is a context option.
|
|
||||||
type ContextOption func(*Context)
|
|
||||||
|
|
||||||
// WithLogf is a shortcut for WithBrowserOption(WithBrowserLogf(f)).
|
|
||||||
func WithLogf(f func(string, ...interface{})) ContextOption {
|
|
||||||
return WithBrowserOption(WithBrowserLogf(f))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithErrorf is a shortcut for WithBrowserOption(WithBrowserErrorf(f)).
|
|
||||||
func WithErrorf(f func(string, ...interface{})) ContextOption {
|
|
||||||
return WithBrowserOption(WithBrowserErrorf(f))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDebugf is a shortcut for WithBrowserOption(WithBrowserDebugf(f)).
|
|
||||||
func WithDebugf(f func(string, ...interface{})) ContextOption {
|
|
||||||
return WithBrowserOption(WithBrowserDebugf(f))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithBrowserOption allows passing a number of browser options to the allocator
|
|
||||||
// when allocating a new browser. As such, this context option can only be used
|
|
||||||
// when NewContext is allocating a new browser.
|
|
||||||
func WithBrowserOption(opts ...BrowserOption) ContextOption {
|
|
||||||
return func(c *Context) {
|
|
||||||
if !c.first {
|
|
||||||
panic("WithBrowserOption can only be used when allocating a new browser")
|
|
||||||
}
|
|
||||||
c.browserOpts = append(c.browserOpts, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Targets lists all the targets in the browser attached to the given context.
|
|
||||||
func Targets(ctx context.Context) ([]*target.Info, error) {
|
|
||||||
// Don't rely on Run, as that needs to be able to call Targets, and we
|
|
||||||
// don't want cyclic func calls.
|
|
||||||
c := FromContext(ctx)
|
|
||||||
if c == nil || c.Allocator == nil {
|
|
||||||
return nil, ErrInvalidContext
|
|
||||||
}
|
|
||||||
if c.Browser == nil {
|
|
||||||
browser, err := c.Allocator.Allocate(ctx, c.browserOpts...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
c.Browser = browser
|
|
||||||
}
|
}
|
||||||
return target.GetTargets().Do(ctx, c.Browser)
|
|
||||||
|
// watch handlers
|
||||||
|
if c.watch == nil {
|
||||||
|
c.watch = c.r.Client().WatchPageTargets(ctxt)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for t := range c.watch {
|
||||||
|
if t == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go c.AddTarget(ctxt, t)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// TODO: fix this
|
||||||
|
timeout := time.After(defaultNewTargetTimeout)
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddTarget adds a target using the supplied context.
|
||||||
|
func (c *CDP) AddTarget(ctxt context.Context, t client.Target) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
// create target manager
|
||||||
|
h, err := NewTargetHandler(t, c.logf, c.debugf, c.errf)
|
||||||
|
if err != nil {
|
||||||
|
c.errf("could not create handler for %s: %v", t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// run
|
||||||
|
if err := h.Run(ctxt); err != nil {
|
||||||
|
c.errf("could not start handler for %s: %v", t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to active handlers
|
||||||
|
c.handlers = append(c.handlers, h)
|
||||||
|
c.handlerMap[t.GetID()] = len(c.handlers) - 1
|
||||||
|
if c.cur == nil {
|
||||||
|
c.cur = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the Chrome runner to terminate.
|
||||||
|
func (c *CDP) Wait() error {
|
||||||
|
c.RLock()
|
||||||
|
r := c.r
|
||||||
|
c.RUnlock()
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.After(DefaultNewTargetTimeout)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
default:
|
||||||
|
var ok bool
|
||||||
|
id := t.GetID()
|
||||||
|
c.RLock()
|
||||||
|
_, ok = c.handlerMap[id]
|
||||||
|
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
|
||||||
|
// index i.
|
||||||
|
func (c *CDP) SetTarget(i int) Action {
|
||||||
|
return ActionFunc(func(context.Context, cdp.Executor) error {
|
||||||
|
return c.SetHandler(i)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTargetByID is an action that sets the active Chrome handler to the handler
|
||||||
|
// 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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != nil {
|
||||||
|
*id = n
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseByIndex closes the Chrome target with specified index i.
|
||||||
|
func (c *CDP) CloseByIndex(i int) Action {
|
||||||
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseByID closes the Chrome target with the specified id.
|
||||||
|
func (c *CDP) CloseByID(id string) Action {
|
||||||
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the action against the current target using the supplied
|
||||||
|
// context.
|
||||||
|
func (c *CDP) Run(ctxt context.Context, a Action) error {
|
||||||
|
c.RLock()
|
||||||
|
cur := c.cur
|
||||||
|
c.RUnlock()
|
||||||
|
|
||||||
|
return a.Do(ctxt, cur)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a Chrome DevTools Protocol option.
|
||||||
|
type Option func(*CDP) error
|
||||||
|
|
||||||
|
// WithRunner is a CDP option to specify the underlying Chrome runner to
|
||||||
|
// monitor for page handlers.
|
||||||
|
func WithRunner(r *runner.Runner) Option {
|
||||||
|
return func(c *CDP) error {
|
||||||
|
c.r = r
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTargets is a CDP option to specify the incoming targets to monitor for
|
||||||
|
// page handlers.
|
||||||
|
func WithTargets(watch <-chan client.Target) Option {
|
||||||
|
return func(c *CDP) error {
|
||||||
|
c.watch = watch
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClient is a CDP option to use the incoming targets from a client.
|
||||||
|
func WithClient(ctxt context.Context, cl *client.Client) Option {
|
||||||
|
return func(c *CDP) error {
|
||||||
|
return WithTargets(cl.WatchPageTargets(ctxt))(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
261
chromedp_test.go
261
chromedp_test.go
@ -2,226 +2,117 @@ package chromedp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"log"
|
||||||
"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
|
||||||
|
|
||||||
browserCtx context.Context
|
defaultContext, defaultCancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
// allocOpts is filled in TestMain
|
cliOpts = []runner.CommandLineOption{
|
||||||
allocOpts []ExecAllocatorOption
|
runner.NoDefaultBrowserCheck,
|
||||||
|
runner.NoFirstRun,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func testAllocate(t *testing.T, path string) (_ context.Context, cancel func()) {
|
func testAllocate(t *testing.T, path string) *Res {
|
||||||
// Same browser, new tab; not needing to start new chrome browsers for
|
c, err := pool.Allocate(defaultContext, cliOpts...)
|
||||||
// each test gives a huge speed-up.
|
if err != nil {
|
||||||
ctx, _ := NewContext(browserCtx)
|
t.Fatalf("could not allocate from pool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 != "" {
|
||||||
if err := Run(ctx, Navigate(testdataDir+"/"+path)); err != nil {
|
err = c.Run(defaultContext, Navigate(testdataDir+"/"+path))
|
||||||
t.Fatal(err)
|
if err != nil {
|
||||||
|
t.Fatalf("could not navigate to testdata/%s: %v", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelErr := func() {
|
return c
|
||||||
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 {
|
||||||
panic(fmt.Sprintf("could not get working directory: %v", err))
|
log.Fatalf("could not get working directory: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
testdataDir = "file://" + path.Join(wd, "testdata")
|
testdataDir = "file://" + path.Join(wd, "testdata")
|
||||||
|
|
||||||
// build on top of the default options
|
// its worth noting that newer versions of chrome (64+) run much faster
|
||||||
allocOpts = append(allocOpts, DefaultExecAllocatorOptions...)
|
|
||||||
|
|
||||||
// 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 ...
|
// than older ones -- same for headless_shell ...
|
||||||
if execPath := os.Getenv("CHROMEDP_TEST_RUNNER"); execPath != "" {
|
execPath := os.Getenv("CHROMEDP_TEST_RUNNER")
|
||||||
allocOpts = append(allocOpts, ExecPath(execPath))
|
if execPath == "" {
|
||||||
|
execPath = runner.LookChromeNames("headless_shell")
|
||||||
}
|
}
|
||||||
|
cliOpts = append(cliOpts, runner.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" {
|
||||||
allocOpts = append(allocOpts, NoSandbox)
|
cliOpts = append(cliOpts, runner.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
allocCtx, cancel := NewExecAllocator(context.Background(), allocOpts...)
|
if targetTimeout := os.Getenv("CHROMEDP_TARGET_TIMEOUT"); targetTimeout != "" {
|
||||||
|
defaultNewTargetTimeout, _ = time.ParseDuration(targetTimeout)
|
||||||
|
}
|
||||||
|
if defaultNewTargetTimeout == 0 {
|
||||||
|
defaultNewTargetTimeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
// start the browser
|
//pool, err = NewPool(PoolLog(log.Printf, log.Printf, log.Printf))
|
||||||
browserCtx, _ = NewContext(allocCtx)
|
pool, err = NewPool()
|
||||||
if err := Run(browserCtx); err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
|
|
||||||
cancel()
|
defaultCancel()
|
||||||
|
|
||||||
|
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)
|
|
||||||
}
|
|
||||||
|
46
client/chrome.go
Normal file
46
client/chrome.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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
Normal file
340
client/client.go
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
174
client/easyjson.go
Normal file
174
client/easyjson.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// 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
Normal file
145
client/gen.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
// +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)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
79
client/targettype.go
Normal file
79
client/targettype.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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)
|
||||||
|
}
|
67
client/transport.go
Normal file
67
client/transport.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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
152
conn.go
@ -1,152 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
34
contrib/meta.sh
Executable file
34
contrib/meta.sh
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
#!/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
|
10
contrib/start.sh
Executable file
10
contrib/start.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/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
|
14
contrib/stats.sh
Executable file
14
contrib/stats.sh
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/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,9 +10,6 @@ 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"
|
||||||
|
|
||||||
@ -42,7 +39,4 @@ 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/cdproto/runtime.RemoteObject,
|
// When res is a type other than *[]byte, or **chromedp/cdp/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(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt 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(ctx, h)
|
v, exp, err := p.Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
100
example_test.go
100
example_test.go
@ -1,100 +0,0 @@
|
|||||||
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,10 +1,9 @@
|
|||||||
module git.loafle.net/commons_go/chromedp
|
module github.com/chromedp/chromedp
|
||||||
|
|
||||||
go 1.12
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a
|
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2
|
||||||
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-20190403194419-1ea4449da983
|
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f
|
||||||
|
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-20190412020601-c4267f5c421a h1:GZPhzysmNSpFnYVSzixFV/ECNILkkn5HJon7AOUNizg=
|
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2 h1:4Ck8YOuS0G3+0xMb80cDSff7QpUolhSc0PGyfagbcdA=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2/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,7 +7,10 @@ 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-20190403194419-1ea4449da983 h1:wL11wNW7dhKIcRCHSm4sHKPWz0tt4mwBsVodG7+Xyqg=
|
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f h1:B6PQkurxGG1rqEX96oE14gbj8bqvYC5dtks9r5uGmlE=
|
||||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f/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
Normal file
657
handler.go
Normal file
@ -0,0 +1,657 @@
|
|||||||
|
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,12 +3,13 @@ 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"
|
||||||
|
|
||||||
"git.loafle.net/commons_go/chromedp/kb"
|
"github.com/chromedp/chromedp/kb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MouseAction is a mouse action.
|
// MouseAction is a mouse action.
|
||||||
@ -26,7 +27,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(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
me := &input.DispatchMouseEventParams{
|
me := &input.DispatchMouseEventParams{
|
||||||
Type: input.MousePressed,
|
Type: input.MousePressed,
|
||||||
X: float64(x),
|
X: float64(x),
|
||||||
@ -40,12 +41,13 @@ func MouseClickXY(x, y int64, opts ...MouseOption) Action {
|
|||||||
me = o(me)
|
me = o(me)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := me.Do(ctx, h); err != nil {
|
err := me.Do(ctxt, h)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
me.Type = input.MouseReleased
|
me.Type = input.MouseReleased
|
||||||
return me.Do(ctx, h)
|
return me.Do(ctxt, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,14 +57,16 @@ 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(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
var pos []int
|
var pos []int
|
||||||
err := EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, n.FullXPath()), &pos).Do(ctx, h)
|
err = EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, n.FullXPath()), &pos).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
box, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx, h)
|
box, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -80,7 +84,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(ctx, h)
|
return MouseClickXY(x, y, opts...).Do(ctxt, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,13 +153,19 @@ 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(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt 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) {
|
||||||
if err := k.Do(ctx, h); err != nil {
|
err = k.Do(ctxt, h)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move to context
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -164,13 +174,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(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
err := dom.Focus().WithNodeID(n.NodeID).Do(ctx, h)
|
err := dom.Focus().WithNodeID(n.NodeID).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyAction(keys, opts...).Do(ctx, h)
|
return KeyAction(keys, opts...).Do(ctxt, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
146
input_test.go
146
input_test.go
@ -4,28 +4,35 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// inViewportJS is a javascript snippet that will get the specified node
|
const (
|
||||||
// position relative to the viewport and returns true if the specified node
|
// inViewportJS is a javascript snippet that will get the specified node
|
||||||
// is within the window's viewport.
|
// position relative to the viewport and returns true if the specified node
|
||||||
const inViewportJS = `(function(a) {
|
// is within the window's viewport.
|
||||||
|
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()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "input.html")
|
var err error
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := Run(ctx, WaitVisible(`#input1`, ByID)); err != nil {
|
c := testAllocate(t, "input.html")
|
||||||
|
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
|
||||||
}{
|
}{
|
||||||
@ -36,14 +43,18 @@ func TestMouseClickXY(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var xstr, ystr string
|
err = c.Run(defaultContext, MouseClickXY(test.x, test.y))
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
var xstr, ystr string
|
||||||
|
err = c.Run(defaultContext, Value("#input1", &xstr, ByID))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
@ -51,10 +62,11 @@ 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)
|
||||||
@ -76,34 +88,40 @@ 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("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "input.html")
|
c := testAllocate(t, "input.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
|
var err error
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||||
|
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 value string
|
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, MouseClickNode(nodes[0], test.opt))
|
||||||
MouseClickNode(nodes[0], test.opt),
|
if err != nil {
|
||||||
Value("#input3", &value, ByID),
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
var value string
|
||||||
|
err = c.Run(defaultContext, Value("#input3", &value, ByID))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -125,42 +143,45 @@ func TestMouseClickOffscreenNode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "input.html")
|
c := testAllocate(t, "input.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
|
var err error
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||||
|
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
|
||||||
if err := Run(ctx, EvaluateAsDevTools(fmt.Sprintf(inViewportJS, nodes[0].FullXPath()), &ok)); err != nil {
|
err = c.Run(defaultContext, EvaluateAsDevTools(fmt.Sprintf(inViewportJS, nodes[0].FullXPath()), &ok))
|
||||||
|
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-- {
|
||||||
if err := Run(ctx, MouseClickNode(nodes[0])); err != nil {
|
err = c.Run(defaultContext, MouseClickNode(nodes[0]))
|
||||||
|
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
|
||||||
if err := Run(ctx, Evaluate("window.document.test_i", &value)); err != nil {
|
err = c.Run(defaultContext, Evaluate("window.document.test_i", &value))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -184,33 +205,37 @@ func TestKeyAction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "input.html")
|
c := testAllocate(t, "input.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
|
var err error
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||||
|
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,
|
|
||||||
Focus(test.sel, test.by),
|
err = c.Run(defaultContext, Focus(test.sel, test.by))
|
||||||
KeyAction(test.exp),
|
if err != nil {
|
||||||
); err != nil {
|
t.Fatalf("got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil {
|
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -234,29 +259,32 @@ func TestKeyActionNode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "input.html")
|
c := testAllocate(t, "input.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
|
var err error
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||||
|
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 value string
|
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, KeyActionNode(nodes[0], test.exp))
|
||||||
KeyActionNode(nodes[0], test.exp),
|
if err != nil {
|
||||||
Value(test.sel, &value, test.by),
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var value string
|
||||||
|
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||||
|
if err != nil {
|
||||||
|
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"
|
||||||
|
|
||||||
"git.loafle.net/commons_go/chromedp/kb"
|
"github.com/chromedp/chromedp/kb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -72,6 +72,8 @@ 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},
|
||||||
@ -80,7 +82,8 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load keys
|
// load keys
|
||||||
if err := loadKeys(keys); err != nil {
|
err = loadKeys(keys)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,19 +94,24 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// output
|
// output
|
||||||
if err := ioutil.WriteFile(*flagOut,
|
err = ioutil.WriteFile(
|
||||||
|
*flagOut,
|
||||||
[]byte(fmt.Sprintf(hdr, *flagPkg, string(constBuf), string(mapBuf))),
|
[]byte(fmt.Sprintf(hdr, *flagPkg, string(constBuf), string(mapBuf))),
|
||||||
0644); err != nil {
|
0644,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// format
|
// format
|
||||||
if err := exec.Command("goimports", "-w", *flagOut).Run(); err != nil {
|
err = exec.Command("goimports", "-w", *flagOut).Run()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// format
|
// format
|
||||||
if err := exec.Command("gofmt", "-s", "-w", *flagOut).Run(); err != nil {
|
err = exec.Command("gofmt", "-s", "-w", *flagOut).Run()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +120,8 @@ 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 {
|
||||||
@ -434,6 +444,8 @@ 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",
|
||||||
|
39
nav.go
39
nav.go
@ -10,9 +10,18 @@ import (
|
|||||||
|
|
||||||
// Navigate navigates the current frame.
|
// Navigate navigates the current frame.
|
||||||
func Navigate(urlstr string) Action {
|
func Navigate(urlstr string) Action {
|
||||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
_, _, _, err := page.Navigate(urlstr).Do(ctx, h)
|
th, ok := h.(*TargetHandler)
|
||||||
return err
|
if !ok {
|
||||||
|
return ErrInvalidHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
frameID, _, _, err := page.Navigate(urlstr).Do(ctxt, th)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return th.SetActive(ctxt, frameID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,9 +32,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(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
var err error
|
var err error
|
||||||
*currentIndex, *entries, err = page.GetNavigationHistory().Do(ctx, h)
|
*currentIndex, *entries, err = page.GetNavigationHistory().Do(ctxt, h)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -38,8 +47,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(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
cur, entries, err := page.GetNavigationHistory().Do(ctx, h)
|
cur, entries, err := page.GetNavigationHistory().Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -48,14 +57,14 @@ func NavigateBack() Action {
|
|||||||
return errors.New("invalid navigation entry")
|
return errors.New("invalid navigation entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.NavigateToHistoryEntry(entries[cur-1].ID).Do(ctx, h)
|
return page.NavigateToHistoryEntry(entries[cur-1].ID).Do(ctxt, 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(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
cur, entries, err := page.GetNavigationHistory().Do(ctx, h)
|
cur, entries, err := page.GetNavigationHistory().Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -64,7 +73,7 @@ func NavigateForward() Action {
|
|||||||
return errors.New("invalid navigation entry")
|
return errors.New("invalid navigation entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.NavigateToHistoryEntry(entries[cur+1].ID).Do(ctx, h)
|
return page.NavigateToHistoryEntry(entries[cur+1].ID).Do(ctxt, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,9 +95,9 @@ func CaptureScreenshot(res *[]byte) Action {
|
|||||||
panic("res cannot be nil")
|
panic("res cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
var err error
|
var err error
|
||||||
*res, err = page.CaptureScreenshot().Do(ctx, h)
|
*res, err = page.CaptureScreenshot().Do(ctxt, h)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -99,9 +108,9 @@ func CaptureScreenshot(res *[]byte) Action {
|
|||||||
panic("id cannot be nil")
|
panic("id cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||||
var err error
|
var err error
|
||||||
*id, err = page.AddScriptToEvaluateOnLoad(source).Do(ctx, h)
|
*id, err = page.AddScriptToEvaluateOnLoad(source).Do(ctxt, h)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
427
nav_test.go
427
nav_test.go
@ -1,43 +1,47 @@
|
|||||||
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()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
var err error
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var urlstr string
|
c := testAllocate(t, "")
|
||||||
if err := Run(ctx,
|
defer c.Release()
|
||||||
WaitVisible(`#icon-brankas`, ByID),
|
|
||||||
Location(&urlstr),
|
expurl, exptitle := testdataDir+"/image.html", "this is title"
|
||||||
); err != nil {
|
|
||||||
|
err = c.Run(defaultContext, Navigate(expurl))
|
||||||
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(urlstr, "image.html") {
|
|
||||||
|
err = c.Run(defaultContext, WaitVisible(`#icon-brankas`, ByID))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var urlstr string
|
||||||
|
err = c.Run(defaultContext, Location(&urlstr))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(urlstr, expurl) {
|
||||||
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
|
||||||
if err := Run(ctx, Title(&title)); err != nil {
|
err = c.Run(defaultContext, Title(&title))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -46,19 +50,21 @@ func TestNavigate(t *testing.T) {
|
|||||||
func TestNavigationEntries(t *testing.T) {
|
func TestNavigationEntries(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "")
|
var err error
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tests := []struct {
|
c := testAllocate(t, "")
|
||||||
file, waitID string
|
defer c.Release()
|
||||||
}{
|
|
||||||
{"form.html", "#form"},
|
tests := []string{
|
||||||
{"image.html", "#icon-brankas"},
|
"form.html",
|
||||||
|
"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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,19 +76,24 @@ func TestNavigationEntries(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expIdx, expEntries := 1, 2
|
expIdx, expEntries := 1, 2
|
||||||
for i, test := range tests {
|
for i, url := range tests {
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, Navigate(testdataDir+"/"+url))
|
||||||
Navigate(testdataDir+"/"+test.file),
|
if err != nil {
|
||||||
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 want := int64(i + 1); index != want {
|
if index != int64(i+1) {
|
||||||
t.Errorf("test %d expected navigation index is %d, got: %d", i, want, index)
|
t.Errorf("test %d expected navigation index is %d, got: %d", i, i, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
expIdx++
|
expIdx++
|
||||||
@ -93,27 +104,42 @@ func TestNavigationEntries(t *testing.T) {
|
|||||||
func TestNavigateToHistoryEntry(t *testing.T) {
|
func TestNavigateToHistoryEntry(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
var err error
|
||||||
defer cancel()
|
|
||||||
|
c := testAllocate(t, "")
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
var entries []*page.NavigationEntry
|
var entries []*page.NavigationEntry
|
||||||
var index int64
|
var index int64
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, Navigate(testdataDir+"/image.html"))
|
||||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
if err != nil {
|
||||||
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
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, Title(&title))
|
||||||
NavigateToHistoryEntry(entries[index].ID),
|
if err != nil {
|
||||||
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 {
|
||||||
@ -124,24 +150,43 @@ func TestNavigateToHistoryEntry(t *testing.T) {
|
|||||||
func TestNavigateBack(t *testing.T) {
|
func TestNavigateBack(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
var err error
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var title, exptitle string
|
c := testAllocate(t, "")
|
||||||
if err := Run(ctx,
|
defer c.Release()
|
||||||
WaitVisible(`#form`, ByID), // for form.html
|
|
||||||
Title(&exptitle),
|
|
||||||
|
|
||||||
Navigate(testdataDir+"/image.html"),
|
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
||||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
if err != nil {
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -150,27 +195,50 @@ func TestNavigateBack(t *testing.T) {
|
|||||||
func TestNavigateForward(t *testing.T) {
|
func TestNavigateForward(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
var err error
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var title, exptitle string
|
c := testAllocate(t, "")
|
||||||
if err := Run(ctx,
|
defer c.Release()
|
||||||
WaitVisible(`#form`, ByID), // for form.html
|
|
||||||
|
|
||||||
Navigate(testdataDir+"/image.html"),
|
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
||||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
if err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -179,9 +247,18 @@ func TestNavigateForward(t *testing.T) {
|
|||||||
func TestStop(t *testing.T) {
|
func TestStop(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
var err error
|
||||||
defer cancel()
|
|
||||||
if err := Run(ctx, Stop()); err != nil {
|
c := testAllocate(t, "")
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,38 +266,36 @@ func TestStop(t *testing.T) {
|
|||||||
func TestReload(t *testing.T) {
|
func TestReload(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
count := 0
|
var err error
|
||||||
// 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()
|
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "")
|
c := testAllocate(t, "")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var title, exptitle string
|
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
||||||
if err := Run(ctx,
|
if err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -229,47 +304,51 @@ func TestReload(t *testing.T) {
|
|||||||
func TestCaptureScreenshot(t *testing.T) {
|
func TestCaptureScreenshot(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
var err error
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// set the viewport size, to know what screenshot size to expect
|
c := testAllocate(t, "")
|
||||||
width, height := 650, 450
|
defer c.Release()
|
||||||
var buf []byte
|
|
||||||
if err := Run(ctx,
|
|
||||||
emulation.SetDeviceMetricsOverride(int64(width), int64(height), 1.0, false),
|
|
||||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
|
||||||
CaptureScreenshot(&buf),
|
|
||||||
); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
config, format, err := image.DecodeConfig(bytes.NewReader(buf))
|
err = c.Run(defaultContext, Navigate(testdataDir+"/image.html"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if want := "png"; format != want {
|
|
||||||
t.Fatalf("expected format to be %q, got %q", want, format)
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
var buf []byte
|
||||||
|
err = c.Run(defaultContext, CaptureScreenshot(&buf))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if config.Width != width || config.Height != height {
|
|
||||||
t.Fatalf("expected dimensions to be %d*%d, got %d*%d",
|
if len(buf) == 0 {
|
||||||
width, height, config.Width, config.Height)
|
t.Fatal("failed to capture screenshot")
|
||||||
}
|
}
|
||||||
|
//TODO: test image
|
||||||
}
|
}
|
||||||
|
|
||||||
/*func TestAddOnLoadScript(t *testing.T) {
|
/*func TestAddOnLoadScript(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "")
|
var err error
|
||||||
defer cancel()
|
|
||||||
|
c := testAllocate(t, "")
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
var scriptID page.ScriptIdentifier
|
var scriptID page.ScriptIdentifier
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, AddOnLoadScript(`window.alert("TEST")`, &scriptID))
|
||||||
AddOnLoadScript(`window.alert("TEST")`, &scriptID),
|
if err != nil {
|
||||||
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")
|
||||||
}
|
}
|
||||||
@ -279,40 +358,57 @@ func TestCaptureScreenshot(t *testing.T) {
|
|||||||
func TestRemoveOnLoadScript(t *testing.T) {
|
func TestRemoveOnLoadScript(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "")
|
var err error
|
||||||
defer cancel()
|
|
||||||
|
c := testAllocate(t, "")
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
var scriptID page.ScriptIdentifier
|
var scriptID page.ScriptIdentifier
|
||||||
if err := Run(ctx, AddOnLoadScript(`window.alert("TEST")`, &scriptID)); err != nil {
|
err = c.Run(defaultContext, AddOnLoadScript(`window.alert("TEST")`, &scriptID))
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, RemoveOnLoadScript(scriptID))
|
||||||
RemoveOnLoadScript(scriptID),
|
if err != nil {
|
||||||
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()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
var err error
|
||||||
defer cancel()
|
expurl := testdataDir + "/form.html"
|
||||||
|
|
||||||
var urlstr string
|
c := testAllocate(t, "")
|
||||||
if err := Run(ctx,
|
defer c.Release()
|
||||||
WaitVisible(`#form`, ByID), // for form.html
|
|
||||||
Location(&urlstr),
|
err = c.Run(defaultContext, Navigate(expurl))
|
||||||
); err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasSuffix(urlstr, "form.html") {
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
var urlstr string
|
||||||
|
err = c.Run(defaultContext, Location(&urlstr))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlstr != expurl {
|
||||||
t.Fatalf("expected to be on form.html, got: %s", urlstr)
|
t.Fatalf("expected to be on form.html, got: %s", urlstr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -320,35 +416,26 @@ func TestLocation(t *testing.T) {
|
|||||||
func TestTitle(t *testing.T) {
|
func TestTitle(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
var err error
|
||||||
defer cancel()
|
expurl, exptitle := testdataDir+"/image.html", "this is title"
|
||||||
|
|
||||||
var title string
|
c := testAllocate(t, "")
|
||||||
if err := Run(ctx,
|
defer c.Release()
|
||||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
|
||||||
Title(&title),
|
err = c.Run(defaultContext, Navigate(expurl))
|
||||||
); err != nil {
|
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)
|
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
Normal file
219
pool.go
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
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
Normal file
47
pool_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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(ctx context.Context, h *Target, n ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return dom.Focus().WithNodeID(nodes[0].NodeID).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(blurJS, nodes[0].FullXPath()), &res).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
*model, err = dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return EvaluateAsDevTools(fmt.Sprintf(textJS, nodes[0].FullXPath()), text).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
errs[i] = a.Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return dom.SetAttributesAsText(nodes[0].NodeID, strings.Join(attrs, " ")).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return dom.SetAttributeValue(nodes[0].NodeID, name, value).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return dom.RemoveAttribute(nodes[0].NodeID, name).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return EvaluateAsDevTools(fmt.Sprintf(attributeJS, nodes[0].FullXPath(), name), res).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(setAttributeJS, nodes[0].FullXPath(), name, value), &res).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return MouseClickNode(nodes[0]).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return MouseClickNode(nodes[0], ClickCount(2)).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return dom.SetFileInputFiles([]string{v}).WithNodeID(n.NodeID).Do(ctxt, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyActionNode(n, v).Do(ctx, h)
|
return KeyActionNode(n, v).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
return dom.SetFileInputFiles(files).WithNodeID(nodes[0].NodeID).Do(ctxt, 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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
box, err := dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctxt, 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(ctx, h)
|
err = EvaluateAsDevTools(fmt.Sprintf(scrollJS, int64(box.Margin[0]), int64(box.Margin[1])), &pos).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// take page screenshot
|
// take page screenshot
|
||||||
buf, err := page.CaptureScreenshot().Do(ctx, h)
|
buf, err := page.CaptureScreenshot().Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -484,7 +484,8 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
|
|||||||
|
|
||||||
// encode
|
// encode
|
||||||
var croppedBuf bytes.Buffer
|
var croppedBuf bytes.Buffer
|
||||||
if err := png.Encode(&croppedBuf, cropped); err != nil {
|
err = png.Encode(&croppedBuf, cropped)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,13 +498,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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(submitJS, nodes[0].FullXPath()), &res).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -519,13 +520,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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(resetJS, nodes[0].FullXPath()), &res).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -544,12 +545,12 @@ func ComputedStyle(sel interface{}, style *[]*css.ComputedProperty, opts ...Quer
|
|||||||
panic("style cannot be nil")
|
panic("style cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
computed, err := css.GetComputedStyleForNode(nodes[0].NodeID).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -567,7 +568,7 @@ func MatchedStyle(sel interface{}, style **css.GetMatchedStylesForNodeReturns, o
|
|||||||
panic("style cannot be nil")
|
panic("style cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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)
|
||||||
}
|
}
|
||||||
@ -576,7 +577,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(ctx, h)
|
err = css.GetMatchedStylesForNode(nodes[0].NodeID).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -589,13 +590,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(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, 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(ctx, h)
|
err := EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, nodes[0].FullXPath()), &pos).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
508
query_test.go
508
query_test.go
@ -1,10 +1,7 @@
|
|||||||
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"
|
||||||
@ -16,16 +13,15 @@ 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"
|
|
||||||
|
|
||||||
"git.loafle.net/commons_go/chromedp/kb"
|
"github.com/chromedp/chromedp/kb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNodes(t *testing.T) {
|
func TestNodes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "table.html")
|
c := testAllocate(t, "table.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -38,12 +34,13 @@ 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
|
||||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
@ -53,8 +50,8 @@ func TestNodes(t *testing.T) {
|
|||||||
func TestNodeIDs(t *testing.T) {
|
func TestNodeIDs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "table.html")
|
c := testAllocate(t, "table.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -67,12 +64,13 @@ 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
|
||||||
if err := Run(ctx, NodeIDs(test.sel, &ids, test.by)); err != nil {
|
err = c.Run(defaultContext, NodeIDs(test.sel, &ids, test.by))
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
@ -82,8 +80,8 @@ func TestNodeIDs(t *testing.T) {
|
|||||||
func TestFocusBlur(t *testing.T) {
|
func TestFocusBlur(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -95,29 +93,35 @@ func TestFocusBlur(t *testing.T) {
|
|||||||
{"#input1", ByID},
|
{"#input1", ByID},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := Run(ctx, Click("#input1", ByID)); err != nil {
|
err := c.Run(defaultContext, Click("#input1", ByID))
|
||||||
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var value string
|
err = c.Run(defaultContext, Focus(test.sel, test.by))
|
||||||
if err := Run(ctx,
|
if err != nil {
|
||||||
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,
|
|
||||||
Blur(test.sel, test.by),
|
err = c.Run(defaultContext, Blur(test.sel, test.by))
|
||||||
Value(test.sel, &value, test.by),
|
if err != nil {
|
||||||
); 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)
|
||||||
}
|
}
|
||||||
@ -127,8 +131,8 @@ func TestFocusBlur(t *testing.T) {
|
|||||||
func TestDimensions(t *testing.T) {
|
func TestDimensions(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
c := testAllocate(t, "image.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -142,12 +146,13 @@ 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
|
||||||
if err := Run(ctx, Dimensions(test.sel, &model)); err != nil {
|
err = c.Run(defaultContext, Dimensions(test.sel, &model))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -157,8 +162,8 @@ func TestDimensions(t *testing.T) {
|
|||||||
func TestText(t *testing.T) {
|
func TestText(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
c := testAllocate(t, "form.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -171,12 +176,13 @@ 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
|
||||||
if err := Run(ctx, Text(test.sel, &text, test.by)); err != nil {
|
err = c.Run(defaultContext, Text(test.sel, &text, test.by))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -208,24 +214,28 @@ func TestClear(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
c := testAllocate(t, "form.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var val string
|
var val string
|
||||||
if err := Run(ctx, Value(test.sel, &val, test.by)); 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 == "" {
|
||||||
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,
|
|
||||||
Clear(test.sel, test.by),
|
err = c.Run(defaultContext, Clear(test.sel, test.by))
|
||||||
Value(test.sel, &val, test.by),
|
if err != nil {
|
||||||
); err != nil {
|
t.Fatalf("got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 != "" {
|
||||||
@ -251,22 +261,27 @@ func TestReset(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
c := testAllocate(t, "form.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var value string
|
err := c.Run(defaultContext, SetValue(test.sel, test.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = c.Run(defaultContext, Reset(test.sel, test.by))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var value string
|
||||||
|
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -277,8 +292,8 @@ func TestReset(t *testing.T) {
|
|||||||
func TestValue(t *testing.T) {
|
func TestValue(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
c := testAllocate(t, "form.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -290,12 +305,13 @@ 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
|
||||||
if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil {
|
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -316,21 +332,22 @@ func TestSetValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
c := testAllocate(t, "form.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var value string
|
err := c.Run(defaultContext, SetValue(test.sel, "FOOBAR", 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var value string
|
||||||
|
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -341,51 +358,45 @@ func TestSetValue(t *testing.T) {
|
|||||||
func TestAttributes(t *testing.T) {
|
func TestAttributes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
c := testAllocate(t, "image.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
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
|
||||||
if err := Run(ctx, Attributes(test.sel, &attrs, test.by)); err != nil {
|
err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by))
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,16 +409,15 @@ func TestAttributes(t *testing.T) {
|
|||||||
func TestAttributesAll(t *testing.T) {
|
func TestAttributesAll(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
c := testAllocate(t, "image.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
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",
|
||||||
@ -423,9 +433,11 @@ 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
|
||||||
if err := Run(ctx, AttributesAll(test.sel, &attrs, test.by)); err != nil {
|
err = c.Run(defaultContext, AttributesAll(test.sel, &attrs, test.by))
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("test %d got error: %v", i, err)
|
t.Fatalf("test %d got error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -444,28 +456,22 @@ 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,
|
||||||
`//*[@id="icon-brankas"]`, BySearch,
|
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: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`,
|
||||||
@ -473,10 +479,8 @@ 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",
|
||||||
@ -484,27 +488,24 @@ 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("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
c := testAllocate(t, "image.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
if err := Run(ctx, SetAttributes(test.sel, test.attrs, test.by)); err != nil {
|
err := c.Run(defaultContext, SetAttributes(test.sel, test.attrs, test.by))
|
||||||
|
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
|
||||||
if err := Run(ctx, Attributes(test.sel, &attrs, test.by)); err != nil {
|
err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by))
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -518,8 +519,8 @@ func TestSetAttributes(t *testing.T) {
|
|||||||
func TestAttributeValue(t *testing.T) {
|
func TestAttributeValue(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
c := testAllocate(t, "image.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -533,15 +534,20 @@ 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)
|
||||||
}
|
}
|
||||||
@ -564,28 +570,27 @@ func TestSetAttributeValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
c := testAllocate(t, "form.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
if err := Run(ctx, SetAttributeValue(test.sel, test.attr, test.exp, test.by)); err != nil {
|
err := c.Run(defaultContext, SetAttributeValue(test.sel, test.attr, test.exp, test.by))
|
||||||
|
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
|
||||||
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("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)
|
||||||
}
|
}
|
||||||
@ -608,23 +613,21 @@ func TestRemoveAttribute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
c := testAllocate(t, "image.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
if err := Run(ctx, RemoveAttribute(test.sel, test.attr)); err != nil {
|
err := c.Run(defaultContext, RemoveAttribute(test.sel, test.attr))
|
||||||
|
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
|
||||||
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("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
if ok || value != "" {
|
if ok || value != "" {
|
||||||
@ -648,22 +651,27 @@ func TestClick(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
c := testAllocate(t, "form.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var title string
|
err := c.Run(defaultContext, Click(test.sel, test.by))
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = c.Run(defaultContext, WaitVisible("#icon-brankas", ByID))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var title string
|
||||||
|
err = c.Run(defaultContext, Title(&title))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -685,21 +693,24 @@ func TestDoubleClick(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var value string
|
err := c.Run(defaultContext, DoubleClick(test.sel, test.by))
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
var value string
|
||||||
|
err = c.Run(defaultContext, Value("#input1", &value, ByID))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -725,21 +736,22 @@ func TestSendKeys(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "visible.html")
|
c := testAllocate(t, "visible.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var val string
|
err := c.Run(defaultContext, SendKeys(test.sel, test.keys, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var val string
|
||||||
|
err = c.Run(defaultContext, Value(test.sel, &val, test.by))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -750,47 +762,31 @@ func TestSendKeys(t *testing.T) {
|
|||||||
func TestScreenshot(t *testing.T) {
|
func TestScreenshot(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
c := testAllocate(t, "image.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
by QueryOption
|
by QueryOption
|
||||||
size int
|
|
||||||
}{
|
}{
|
||||||
{"/html/body/img", BySearch, 239},
|
{"/html/body/img", BySearch},
|
||||||
{"img", ByQueryAll, 239},
|
{"img", ByQueryAll},
|
||||||
{"#icon-github", ByID, 120},
|
{"img", ByQuery},
|
||||||
}
|
{"#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
|
||||||
if err := Run(ctx, Screenshot(test.sel, &buf)); err != nil {
|
err = c.Run(defaultContext, Screenshot(test.sel, &buf))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
config, format, err := image.DecodeConfig(bytes.NewReader(buf))
|
//TODO: test image
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -808,22 +804,27 @@ func TestSubmit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "form.html")
|
c := testAllocate(t, "form.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var title string
|
err := c.Run(defaultContext, Submit(test.sel, test.by))
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = c.Run(defaultContext, WaitVisible("#icon-brankas", ByID))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var title string
|
||||||
|
err = c.Run(defaultContext, Title(&title))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -845,15 +846,17 @@ func TestComputedStyle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
var styles []*css.ComputedProperty
|
var styles []*css.ComputedProperty
|
||||||
if err := Run(ctx, ComputedStyle(test.sel, &styles, test.by)); err != nil {
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -864,10 +867,16 @@ func TestComputedStyle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := Run(ctx,
|
|
||||||
Click("#input1", ByID),
|
err = c.Run(defaultContext, Click("#input1", ByID))
|
||||||
ComputedStyle(test.sel, &styles, test.by),
|
if err != nil {
|
||||||
); err != nil {
|
t.Fatalf("got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -896,15 +905,17 @@ func TestMatchedStyle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
test := test
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
var styles *css.GetMatchedStylesForNodeReturns
|
var styles *css.GetMatchedStylesForNodeReturns
|
||||||
if err := Run(ctx, MatchedStyle(test.sel, &styles, test.by)); err != nil {
|
err := c.Run(defaultContext, MatchedStyle(test.sel, &styles, test.by))
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -919,7 +930,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, "%s", uploadHTML)
|
fmt.Fprintf(res, 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")
|
||||||
@ -946,11 +957,10 @@ func TestFileUpload(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.Remove(tmpfile.Name())
|
defer os.Remove(tmpfile.Name())
|
||||||
defer tmpfile.Close()
|
if _, err = tmpfile.WriteString(uploadHTML); err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -961,26 +971,25 @@ 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("%02d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
ctx, cancel := testAllocate(t, "")
|
// TODO: refactor the test so the subtests can run in
|
||||||
defer cancel()
|
// parallel
|
||||||
|
//t.Parallel()
|
||||||
|
|
||||||
|
c := testAllocate(t, "")
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
var result string
|
var result string
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, Tasks{
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -991,8 +1000,8 @@ func TestFileUpload(t *testing.T) {
|
|||||||
func TestInnerHTML(t *testing.T) {
|
func TestInnerHTML(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "table.html")
|
c := testAllocate(t, "table.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -1002,12 +1011,13 @@ 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
|
||||||
if err := Run(ctx, InnerHTML(test.sel, &html)); err != nil {
|
err = c.Run(defaultContext, InnerHTML(test.sel, &html))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -1017,8 +1027,8 @@ func TestInnerHTML(t *testing.T) {
|
|||||||
func TestOuterHTML(t *testing.T) {
|
func TestOuterHTML(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "table.html")
|
c := testAllocate(t, "table.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -1028,12 +1038,13 @@ 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
|
||||||
if err := Run(ctx, OuterHTML(test.sel, &html)); err != nil {
|
err = c.Run(defaultContext, OuterHTML(test.sel, &html))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -1043,8 +1054,8 @@ func TestOuterHTML(t *testing.T) {
|
|||||||
func TestScrollIntoView(t *testing.T) {
|
func TestScrollIntoView(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "image.html")
|
c := testAllocate(t, "image.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sel string
|
sel string
|
||||||
@ -1055,11 +1066,12 @@ 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 {
|
||||||
if err := Run(ctx, ScrollIntoView(test.sel, test.by)); err != nil {
|
err = c.Run(defaultContext, ScrollIntoView(test.sel, test.by))
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
runner/path_darwin.go
Normal file
13
runner/path_darwin.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// +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
|
19
runner/path_unix.go
Normal file
19
runner/path_unix.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// +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",
|
||||||
|
}
|
13
runner/path_windows.go
Normal file
13
runner/path_windows.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// +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
Normal file
482
runner/runner.go
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
// 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
|
||||||
|
}
|
11
runner/runner_bsd.go
Normal file
11
runner/runner_bsd.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// +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
|
||||||
|
}
|
87
runner/runner_linux.go
Normal file
87
runner/runner_linux.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// +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)
|
||||||
|
}
|
31
runner/runner_unix.go
Normal file
31
runner/runner_unix.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// +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)
|
||||||
|
}
|
26
runner/runner_windows.go
Normal file
26
runner/runner_windows.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// +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
|
||||||
|
}
|
178
sel.go
178
sel.go
@ -5,6 +5,7 @@ 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"
|
||||||
@ -25,9 +26,9 @@ tagname
|
|||||||
type Selector struct {
|
type Selector struct {
|
||||||
sel interface{}
|
sel interface{}
|
||||||
exp int
|
exp int
|
||||||
by func(context.Context, *Target, *cdp.Node) ([]cdp.NodeID, error)
|
by func(context.Context, *TargetHandler, *cdp.Node) ([]cdp.NodeID, error)
|
||||||
wait func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error)
|
wait func(context.Context, *TargetHandler, *cdp.Node, ...cdp.NodeID) ([]*cdp.Node, error)
|
||||||
after func(context.Context, *Target, ...*cdp.Node) error
|
after func(context.Context, *TargetHandler, ...*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
|
||||||
@ -55,17 +56,21 @@ func Query(sel interface{}, opts ...QueryOption) Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do satisfies the Action interface.
|
// Do satisfies the Action interface.
|
||||||
func (s *Selector) Do(ctx context.Context, h cdp.Executor) error {
|
func (s *Selector) Do(ctxt context.Context, h cdp.Executor) error {
|
||||||
th, ok := h.(*Target)
|
th, ok := h.(*TargetHandler)
|
||||||
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(ctx, th):
|
case err = <-s.run(ctxt, th):
|
||||||
case <-ctx.Done():
|
case <-ctxt.Done():
|
||||||
err = ctx.Err()
|
err = ctxt.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
@ -74,35 +79,54 @@ func (s *Selector) Do(ctx 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(ctx context.Context, h *Target) chan error {
|
func (s *Selector) run(ctxt context.Context, h *TargetHandler) 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()
|
|
||||||
|
|
||||||
if root == nil {
|
go func() {
|
||||||
// not ready?
|
defer close(ch)
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
ids, err := s.by(ctx, h, root)
|
for {
|
||||||
if err != nil || len(ids) < s.exp {
|
root, err := h.GetRoot(ctxt)
|
||||||
return false
|
if err != nil {
|
||||||
}
|
select {
|
||||||
nodes, err := s.wait(ctx, h, cur, ids...)
|
case <-ctxt.Done():
|
||||||
// if nodes==nil, we're not yet ready
|
ch <- ctxt.Err()
|
||||||
if nodes == nil || err != nil {
|
return
|
||||||
return false
|
default:
|
||||||
}
|
continue
|
||||||
if s.after != nil {
|
}
|
||||||
if err := s.after(ctx, h, nodes...); err != nil {
|
}
|
||||||
ch <- err
|
|
||||||
|
select {
|
||||||
|
default:
|
||||||
|
ids, err := s.by(ctxt, h, root)
|
||||||
|
if err == nil && len(ids) >= s.exp {
|
||||||
|
nodes, err := s.wait(ctxt, h, root, ids...)
|
||||||
|
if err == nil {
|
||||||
|
if s.after == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.after(ctxt, h, nodes...)
|
||||||
|
if err != nil {
|
||||||
|
ch <- err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(DefaultCheckDuration)
|
||||||
|
|
||||||
|
case <-root.Invalidated:
|
||||||
|
continue
|
||||||
|
|
||||||
|
case <-ctxt.Done():
|
||||||
|
ch <- ctxt.Err()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(ch)
|
}()
|
||||||
return true
|
|
||||||
}
|
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,10 +139,20 @@ 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, *Target, ...*cdp.Node) error, opts ...QueryOption) Action {
|
func QueryAfter(sel interface{}, f func(context.Context, *TargetHandler, ...*cdp.Node) error, opts ...QueryOption) Action {
|
||||||
return Query(sel, append(opts, After(f))...)
|
return Query(sel, append(opts, After(f))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +160,7 @@ func QueryAfter(sel interface{}, f func(context.Context, *Target, ...*cdp.Node)
|
|||||||
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, *Target, *cdp.Node) ([]cdp.NodeID, error)) QueryOption {
|
func ByFunc(f func(context.Context, *TargetHandler, *cdp.Node) ([]cdp.NodeID, error)) QueryOption {
|
||||||
return func(s *Selector) {
|
return func(s *Selector) {
|
||||||
s.by = f
|
s.by = f
|
||||||
}
|
}
|
||||||
@ -135,8 +169,8 @@ func ByFunc(f func(context.Context, *Target, *cdp.Node) ([]cdp.NodeID, error)) Q
|
|||||||
// 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(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||||
nodeID, err := dom.QuerySelector(n.NodeID, s.selAsString()).Do(ctx, h)
|
nodeID, err := dom.QuerySelector(n.NodeID, s.selAsString()).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -151,8 +185,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(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||||
return dom.QuerySelectorAll(n.NodeID, s.selAsString()).Do(ctx, h)
|
return dom.QuerySelectorAll(n.NodeID, s.selAsString()).Do(ctxt, h)
|
||||||
})(s)
|
})(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,8 +199,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(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||||
id, count, err := dom.PerformSearch(s.selAsString()).Do(ctx, h)
|
id, count, err := dom.PerformSearch(s.selAsString()).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -175,7 +209,7 @@ func BySearch(s *Selector) {
|
|||||||
return []cdp.NodeID{}, nil
|
return []cdp.NodeID{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes, err := dom.GetSearchResults(id, 0, count).Do(ctx, h)
|
nodes, err := dom.GetSearchResults(id, 0, count).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -191,9 +225,9 @@ func ByNodeID(s *Selector) {
|
|||||||
panic("ByNodeID can only work on []cdp.NodeID")
|
panic("ByNodeID can only work on []cdp.NodeID")
|
||||||
}
|
}
|
||||||
|
|
||||||
ByFunc(func(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
err := dom.RequestChildNodes(id).WithPierce(true).Do(ctx, h)
|
err := dom.RequestChildNodes(id).WithPierce(true).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -204,28 +238,38 @@ 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, *Target, *cdp.Node) error) func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error) {
|
func (s *Selector) waitReady(check func(context.Context, *TargetHandler, *cdp.Node) error) func(context.Context, *TargetHandler, *cdp.Node, ...cdp.NodeID) ([]*cdp.Node, error) {
|
||||||
return func(ctx context.Context, h *Target, cur *cdp.Frame, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
return func(ctxt context.Context, h *TargetHandler, n *cdp.Node, 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))
|
||||||
cur.RLock()
|
errs := make([]error, len(ids))
|
||||||
for i, id := range ids {
|
for i, id := range ids {
|
||||||
nodes[i] = cur.Nodes[id]
|
wg.Add(1)
|
||||||
if nodes[i] == nil {
|
go func(i int, id cdp.NodeID) {
|
||||||
cur.RUnlock()
|
defer wg.Done()
|
||||||
// not yet ready
|
nodes[i], errs[i] = h.WaitNode(ctxt, f, id)
|
||||||
return nil, nil
|
}(i, id)
|
||||||
|
}
|
||||||
|
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(ctx, h, n)
|
errs[i] = check(ctxt, h, n)
|
||||||
}(i, n)
|
}(i, n)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@ -242,7 +286,7 @@ func (s *Selector) waitReady(check func(context.Context, *Target, *cdp.Node) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error)) QueryOption {
|
func WaitFunc(wait func(context.Context, *TargetHandler, *cdp.Node, ...cdp.NodeID) ([]*cdp.Node, error)) QueryOption {
|
||||||
return func(s *Selector) {
|
return func(s *Selector) {
|
||||||
s.wait = wait
|
s.wait = wait
|
||||||
}
|
}
|
||||||
@ -255,9 +299,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(ctx context.Context, h *Target, n *cdp.Node) error {
|
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
||||||
// check box model
|
// check box model
|
||||||
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx, h)
|
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isCouldNotComputeBoxModelError(err) {
|
if isCouldNotComputeBoxModelError(err) {
|
||||||
return ErrNotVisible
|
return ErrNotVisible
|
||||||
@ -268,7 +312,7 @@ func NodeVisible(s *Selector) {
|
|||||||
|
|
||||||
// check offsetParent
|
// check offsetParent
|
||||||
var res bool
|
var res bool
|
||||||
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctx, h)
|
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -281,9 +325,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(ctx context.Context, h *Target, n *cdp.Node) error {
|
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
||||||
// check box model
|
// check box model
|
||||||
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx, h)
|
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isCouldNotComputeBoxModelError(err) {
|
if isCouldNotComputeBoxModelError(err) {
|
||||||
return nil
|
return nil
|
||||||
@ -294,7 +338,7 @@ func NodeNotVisible(s *Selector) {
|
|||||||
|
|
||||||
// check offsetParent
|
// check offsetParent
|
||||||
var res bool
|
var res bool
|
||||||
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctx, h)
|
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -307,7 +351,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(ctx context.Context, h *Target, n *cdp.Node) error {
|
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
||||||
n.RLock()
|
n.RLock()
|
||||||
defer n.RUnlock()
|
defer n.RUnlock()
|
||||||
|
|
||||||
@ -323,7 +367,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(ctx context.Context, h *Target, n *cdp.Node) error {
|
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
||||||
n.RLock()
|
n.RLock()
|
||||||
defer n.RUnlock()
|
defer n.RUnlock()
|
||||||
|
|
||||||
@ -337,11 +381,11 @@ func NodeSelected(s *Selector) {
|
|||||||
}))(s)
|
}))(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeNotPresent is a query option to wait until no elements are present
|
// NodeNotPresent is a query option to wait until no elements match are
|
||||||
// matching the selector.
|
// present matching the selector.
|
||||||
func NodeNotPresent(s *Selector) {
|
func NodeNotPresent(s *Selector) {
|
||||||
s.exp = 0
|
s.exp = 0
|
||||||
WaitFunc(func(ctx context.Context, h *Target, cur *cdp.Frame, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
WaitFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
||||||
if len(ids) != 0 {
|
if len(ids) != 0 {
|
||||||
return nil, ErrHasResults
|
return nil, ErrHasResults
|
||||||
}
|
}
|
||||||
@ -359,7 +403,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, *Target, ...*cdp.Node) error) QueryOption {
|
func After(f func(context.Context, *TargetHandler, ...*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,21 +9,26 @@ import (
|
|||||||
func TestWaitReady(t *testing.T) {
|
func TestWaitReady(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var nodeIDs []cdp.NodeID
|
var nodeIDs []cdp.NodeID
|
||||||
if err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)); err != nil {
|
err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID))
|
||||||
|
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
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID))
|
||||||
WaitReady("#input2", ByID),
|
if err != nil {
|
||||||
Value(nodeIDs, &value, ByNodeID),
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -31,21 +36,26 @@ func TestWaitReady(t *testing.T) {
|
|||||||
func TestWaitVisible(t *testing.T) {
|
func TestWaitVisible(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var nodeIDs []cdp.NodeID
|
var nodeIDs []cdp.NodeID
|
||||||
if err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)); err != nil {
|
err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID))
|
||||||
|
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
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID))
|
||||||
WaitVisible("#input2", ByID),
|
if err != nil {
|
||||||
Value(nodeIDs, &value, ByNodeID),
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,22 +63,31 @@ func TestWaitVisible(t *testing.T) {
|
|||||||
func TestWaitNotVisible(t *testing.T) {
|
func TestWaitNotVisible(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var nodeIDs []cdp.NodeID
|
var nodeIDs []cdp.NodeID
|
||||||
if err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)); err != nil {
|
err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID))
|
||||||
|
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
|
||||||
if err := Run(ctx,
|
err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID))
|
||||||
Click("#button2", ByID),
|
if err != nil {
|
||||||
WaitNotVisible("#input2", ByID),
|
|
||||||
Value(nodeIDs, &value, ByNodeID),
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,35 +95,46 @@ func TestWaitNotVisible(t *testing.T) {
|
|||||||
func TestWaitEnabled(t *testing.T) {
|
func TestWaitEnabled(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var attr string
|
var attr string
|
||||||
var ok bool
|
var ok bool
|
||||||
if err := Run(ctx, AttributeValue("#select1", "disabled", &attr, &ok, ByID)); err != nil {
|
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 disabled")
|
t.Fatal("expected element to be disabled")
|
||||||
}
|
}
|
||||||
if err := Run(ctx,
|
|
||||||
Click("#button3", ByID),
|
err = c.Run(defaultContext, Click("#button3", ByID))
|
||||||
WaitEnabled("#select1", ByID),
|
if err != nil {
|
||||||
AttributeValue("#select1", "disabled", &attr, &ok, ByID),
|
t.Fatalf("got error: %v", err)
|
||||||
); 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")
|
||||||
}
|
}
|
||||||
var value string
|
|
||||||
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 {
|
||||||
Value("#select1", &value, ByID),
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("got error: %v", err)
|
t.Fatalf("got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var value string
|
||||||
|
err = c.Run(defaultContext, Value("#select1", &value, ByID))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -113,32 +143,43 @@ func TestWaitEnabled(t *testing.T) {
|
|||||||
func TestWaitSelected(t *testing.T) {
|
func TestWaitSelected(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
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)
|
||||||
); 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
|
||||||
if err := Run(ctx, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, &ok)); err != nil {
|
err = c.Run(defaultContext, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, &ok))
|
||||||
|
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,
|
|
||||||
SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"),
|
err = c.Run(defaultContext, SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"))
|
||||||
WaitSelected(`//*[@id="select1"]/option[1]`),
|
if err != nil {
|
||||||
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")
|
||||||
}
|
}
|
||||||
@ -147,14 +188,21 @@ func TestWaitSelected(t *testing.T) {
|
|||||||
func TestWaitNotPresent(t *testing.T) {
|
func TestWaitNotPresent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
if err := Run(ctx,
|
err := c.Run(defaultContext, WaitVisible("#input3", ByID))
|
||||||
WaitVisible("#input3", ByID),
|
if err != nil {
|
||||||
Click("#button4", ByID),
|
t.Fatalf("got error: %v", err)
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,11 +210,12 @@ func TestWaitNotPresent(t *testing.T) {
|
|||||||
func TestAtLeast(t *testing.T) {
|
func TestAtLeast(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cancel := testAllocate(t, "js.html")
|
c := testAllocate(t, "js.html")
|
||||||
defer cancel()
|
defer c.Release()
|
||||||
|
|
||||||
var nodes []*cdp.Node
|
var nodes []*cdp.Node
|
||||||
if err := Run(ctx, Nodes("//input", &nodes, AtLeast(3))); err != nil {
|
err := c.Run(defaultContext, Nodes("//input", &nodes, AtLeast(3)))
|
||||||
|
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
320
target.go
@ -1,320 +0,0 @@
|
|||||||
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
9
testdata/iframe.html
vendored
@ -1,9 +0,0 @@
|
|||||||
<!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