Compare commits
62 Commits
Author | SHA1 | Date | |
---|---|---|---|
ad82438599 | |||
a0bba77505 | |||
|
f606ca9e73 | ||
|
1a54253acd | ||
|
958088f83b | ||
|
e4c16681d0 | ||
|
71ae9f7bbc | ||
|
ac47d6ba0e | ||
|
92a77355f6 | ||
|
46982a1cac | ||
|
e8122e4a26 | ||
|
b481eeac51 | ||
|
b8efcf0691 | ||
|
a29b1ec1d6 | ||
|
11b3a5dc8f | ||
|
d0484ed1c5 | ||
|
687cf6d766 | ||
|
939d377090 | ||
|
b977e305d2 | ||
|
c41ed01b6a | ||
|
c313fa1c1d | ||
|
b647c708b4 | ||
|
97e80a00d5 | ||
|
504561eab2 | ||
|
65a198c84e | ||
|
ece2b3ab92 | ||
|
896fbe60c2 | ||
|
e482cdfc4d | ||
|
120628a01c | ||
|
7c8529b914 | ||
|
41e913e571 | ||
|
ad8809efb7 | ||
|
0d568ec2a4 | ||
|
fb23c1750a | ||
|
d73caffcd0 | ||
|
1decbccd74 | ||
|
117274bc5d | ||
|
661ef78880 | ||
|
8ff2971fc5 | ||
|
a0a36956a8 | ||
|
2b925df0fb | ||
|
f742f327a7 | ||
|
0e92de5e65 | ||
|
32d4bae280 | ||
|
a93c63124f | ||
|
b136a6267e | ||
|
e698c943b3 | ||
|
5fb1c07412 | ||
|
2ca3ea3591 | ||
|
6fb5264bbd | ||
|
da4ac414ed | ||
|
7c1a9fbf3e | ||
|
c109f6ebfd | ||
|
61f0a8da68 | ||
|
92bfcc3c8d | ||
|
24decf54d3 | ||
|
81a48280ef | ||
|
3d3bf22ccc | ||
|
5aca12cc3e | ||
|
e9aa66f87e | ||
|
39bd95c850 | ||
|
b61de69d62 |
2
.github/ISSUE_TEMPLATE
vendored
2
.github/ISSUE_TEMPLATE
vendored
@ -1,7 +1,7 @@
|
||||
#### What versions are you running?
|
||||
|
||||
<pre>
|
||||
$ go list -m github.com/chromedp/chromedp
|
||||
$ go list -m git.loafle.net/commons_go/chromedp
|
||||
$ chromium --version
|
||||
$ go version
|
||||
</pre>
|
||||
|
13
.travis.yml
13
.travis.yml
@ -1,14 +1,11 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
|
||||
addons:
|
||||
apt:
|
||||
chrome: stable
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls golang.org/x/vgo
|
||||
|
||||
script:
|
||||
- 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
|
||||
- go test -v ./...
|
||||
|
12
README.md
12
README.md
@ -9,13 +9,14 @@ Package chromedp is a faster, simpler way to drive browsers supporting the
|
||||
Install in the usual Go way:
|
||||
|
||||
```sh
|
||||
go get -u github.com/chromedp/chromedp
|
||||
go get -u git.loafle.net/commons_go/chromedp
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Please see the [examples][6] project for more examples. Please refer to the
|
||||
[GoDoc API listing][7] for a summary of the API and Actions.
|
||||
[GoDoc API listing][7] for a summary of the API and Actions, which also contains
|
||||
a few simple and runnable examples.
|
||||
|
||||
## Resources
|
||||
|
||||
@ -24,11 +25,10 @@ Please see the [examples][6] project for more examples. Please refer to the
|
||||
* [chromedp examples][6] - various `chromedp` examples
|
||||
* [`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/chromedp-proxy`][11] - a simple CDP proxy for logging CDP clients and browsers
|
||||
* [`git.loafle.net/commons_go/chromedp-proxy`][11] - a simple CDP proxy for logging CDP clients and browsers
|
||||
|
||||
## TODO
|
||||
|
||||
* Move timeouts to context (defaults)
|
||||
* Implement more query selector options (allow over riding context timeouts)
|
||||
* Contextual actions for "dry run" (or via an accumulator?)
|
||||
* Network loader / manager
|
||||
@ -40,8 +40,8 @@ Please see the [examples][6] project for more examples. Please refer to the
|
||||
[4]: https://coveralls.io/github/chromedp/chromedp?branch=master
|
||||
[5]: https://chromedevtools.github.io/devtools-protocol/
|
||||
[6]: https://github.com/chromedp/examples
|
||||
[7]: https://godoc.org/github.com/chromedp/chromedp
|
||||
[7]: https://godoc.org/git.loafle.net/commons_go/chromedp
|
||||
[8]: https://www.youtube.com/watch?v=_7pWCg94sKw
|
||||
[9]: https://godoc.org/github.com/chromedp/cdproto
|
||||
[10]: https://github.com/chromedp/cdproto-gen
|
||||
[11]: https://github.com/chromedp/chromedp-proxy
|
||||
[11]: https://git.loafle.net/commons_go/chromedp-proxy
|
||||
|
23
actions.go
23
actions.go
@ -18,8 +18,8 @@ type Action interface {
|
||||
type ActionFunc func(context.Context, cdp.Executor) error
|
||||
|
||||
// Do executes the func f using the provided context and frame handler.
|
||||
func (f ActionFunc) Do(ctxt context.Context, h cdp.Executor) error {
|
||||
return f(ctxt, h)
|
||||
func (f ActionFunc) Do(ctx context.Context, h cdp.Executor) error {
|
||||
return f(ctx, h)
|
||||
}
|
||||
|
||||
// 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
|
||||
// frame handler.
|
||||
func (t Tasks) Do(ctxt context.Context, h cdp.Executor) error {
|
||||
func (t Tasks) Do(ctx context.Context, h cdp.Executor) error {
|
||||
// TODO: put individual task timeouts from context here
|
||||
for _, a := range t {
|
||||
// ctxt, cancel = context.WithTimeout(ctxt, timeout)
|
||||
// ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||
// defer cancel()
|
||||
if err := a.Do(ctxt, h); err != nil {
|
||||
if err := a.Do(ctx, h); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -46,12 +46,15 @@ func (t Tasks) Do(ctxt context.Context, h cdp.Executor) error {
|
||||
// be marked for deprecation in the future, after the remaining Actions have
|
||||
// been able to be written/tested.
|
||||
func Sleep(d time.Duration) Action {
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
// Don't use time.After, to avoid a temporary goroutine leak if
|
||||
// ctx is cancelled before the timer fires.
|
||||
t := time.NewTimer(d)
|
||||
select {
|
||||
case <-time.After(d):
|
||||
|
||||
case <-ctxt.Done():
|
||||
return ctxt.Err()
|
||||
case <-t.C:
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
return ctx.Err()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
288
allocate.go
Normal file
288
allocate.go
Normal file
@ -0,0 +1,288 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// An Allocator is responsible for creating and managing a number of browsers.
|
||||
//
|
||||
// This interface abstracts away how the browser process is actually run. For
|
||||
// example, an Allocator implementation may reuse browser processes, or connect
|
||||
// to already-running browsers on remote machines.
|
||||
type Allocator interface {
|
||||
// Allocate creates a new browser. It can be cancelled via the provided
|
||||
// context, at which point all the resources used by the browser (such
|
||||
// as temporary directories) will be freed.
|
||||
Allocate(context.Context, ...BrowserOption) (*Browser, error)
|
||||
|
||||
// Wait blocks until an allocator has freed all of its resources.
|
||||
// Cancelling the allocator context will already perform this operation,
|
||||
// so normally there's no need to call Wait directly.
|
||||
Wait()
|
||||
}
|
||||
|
||||
// setupExecAllocator is similar to NewExecAllocator, but it allows NewContext
|
||||
// to create the allocator without the unnecessary context layer.
|
||||
func setupExecAllocator(opts ...ExecAllocatorOption) *ExecAllocator {
|
||||
ep := &ExecAllocator{
|
||||
initFlags: make(map[string]interface{}),
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(ep)
|
||||
}
|
||||
if ep.execPath == "" {
|
||||
ep.execPath = findExecPath()
|
||||
}
|
||||
return ep
|
||||
}
|
||||
|
||||
// DefaultExecAllocatorOptions are the ExecAllocator options used by NewContext
|
||||
// if the given parent context doesn't have an allocator set up.
|
||||
var DefaultExecAllocatorOptions = []ExecAllocatorOption{
|
||||
NoFirstRun,
|
||||
NoDefaultBrowserCheck,
|
||||
Headless,
|
||||
}
|
||||
|
||||
// NewExecAllocator creates a new context set up with an ExecAllocator, suitable
|
||||
// for use with NewContext.
|
||||
func NewExecAllocator(parent context.Context, opts ...ExecAllocatorOption) (context.Context, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
c := &Context{Allocator: setupExecAllocator(opts...)}
|
||||
|
||||
ctx = context.WithValue(ctx, contextKey{}, c)
|
||||
cancelWait := func() {
|
||||
cancel()
|
||||
c.Allocator.Wait()
|
||||
}
|
||||
return ctx, cancelWait
|
||||
}
|
||||
|
||||
// ExecAllocatorOption is a exec allocator option.
|
||||
type ExecAllocatorOption func(*ExecAllocator)
|
||||
|
||||
// ExecAllocator is an Allocator which starts new browser processes on the host
|
||||
// machine.
|
||||
type ExecAllocator struct {
|
||||
execPath string
|
||||
initFlags map[string]interface{}
|
||||
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// Allocate satisfies the Allocator interface.
|
||||
func (p *ExecAllocator) Allocate(ctx context.Context, opts ...BrowserOption) (*Browser, error) {
|
||||
c := FromContext(ctx)
|
||||
if c == nil {
|
||||
return nil, ErrInvalidContext
|
||||
}
|
||||
|
||||
var args []string
|
||||
for name, value := range p.initFlags {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
args = append(args, fmt.Sprintf("--%s=%s", name, value))
|
||||
case bool:
|
||||
if value {
|
||||
args = append(args, fmt.Sprintf("--%s", name))
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid exec pool flag")
|
||||
}
|
||||
}
|
||||
|
||||
removeDir := false
|
||||
dataDir, ok := p.initFlags["user-data-dir"].(string)
|
||||
if !ok {
|
||||
tempDir, err := ioutil.TempDir("", "chromedp-runner")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, "--user-data-dir="+tempDir)
|
||||
dataDir = tempDir
|
||||
removeDir = true
|
||||
}
|
||||
args = append(args, "--remote-debugging-port=0")
|
||||
|
||||
var cmd *exec.Cmd
|
||||
p.wg.Add(1) // for the entire allocator
|
||||
c.wg.Add(1) // for this browser's root context
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
// First wait for the process to be finished.
|
||||
if cmd != nil {
|
||||
// TODO: do we care about this error in any scenario? if
|
||||
// the user cancelled the context and killed chrome,
|
||||
// this will most likely just be "signal: killed", which
|
||||
// isn't interesting.
|
||||
cmd.Wait()
|
||||
}
|
||||
// Then delete the temporary user data directory, if needed.
|
||||
if removeDir {
|
||||
if err := os.RemoveAll(dataDir); c.cancelErr == nil {
|
||||
c.cancelErr = err
|
||||
}
|
||||
}
|
||||
p.wg.Done()
|
||||
c.wg.Done()
|
||||
}()
|
||||
|
||||
// force the first page to be blank, instead of the welcome page
|
||||
// TODO: why isn't --no-first-run enough?
|
||||
args = append(args, "about:blank")
|
||||
|
||||
cmd = exec.CommandContext(ctx, p.execPath, args...)
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pick up the browser's websocket URL from stderr.
|
||||
wsURL := ""
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
prefix := "DevTools listening on"
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if s := strings.TrimPrefix(line, prefix); s != line {
|
||||
wsURL = strings.TrimSpace(s)
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stderr.Close()
|
||||
|
||||
browser, err := NewBrowser(ctx, wsURL, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
browser.process = cmd.Process
|
||||
browser.userDataDir = dataDir
|
||||
return browser, nil
|
||||
}
|
||||
|
||||
// Wait satisfies the Allocator interface.
|
||||
func (p *ExecAllocator) Wait() {
|
||||
p.wg.Wait()
|
||||
}
|
||||
|
||||
// ExecPath returns an ExecAllocatorOption which uses the given path to execute
|
||||
// browser processes. The given path can be an absolute path to a binary, or
|
||||
// just the name of the program to find via exec.LookPath.
|
||||
func ExecPath(path string) ExecAllocatorOption {
|
||||
return func(p *ExecAllocator) {
|
||||
if fullPath, _ := exec.LookPath(path); fullPath != "" {
|
||||
// Convert to an absolute path if possible, to avoid
|
||||
// repeated LookPath calls in each Allocate.
|
||||
path = fullPath
|
||||
}
|
||||
p.execPath = path
|
||||
}
|
||||
}
|
||||
|
||||
// findExecPath tries to find the Chrome browser somewhere in the current
|
||||
// system. It performs a rather agressive search, which is the same in all
|
||||
// systems. That may make it a bit slow, but it will only be run when creating a
|
||||
// new ExecAllocator.
|
||||
func findExecPath() string {
|
||||
for _, path := range [...]string{
|
||||
// Unix-like
|
||||
"headless_shell",
|
||||
"headless-shell",
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
"google-chrome",
|
||||
"google-chrome-stable",
|
||||
"google-chrome-beta",
|
||||
"google-chrome-unstable",
|
||||
"/usr/bin/google-chrome",
|
||||
|
||||
// Windows
|
||||
"chrome",
|
||||
"chrome.exe", // in case PATHEXT is misconfigured
|
||||
`C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`,
|
||||
|
||||
// Mac
|
||||
`/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
||||
} {
|
||||
found, err := exec.LookPath(path)
|
||||
if err == nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
// Fall back to something simple and sensible, to give a useful error
|
||||
// message.
|
||||
return "google-chrome"
|
||||
}
|
||||
|
||||
// Flag is a generic command line option to pass a flag to Chrome. If the value
|
||||
// is a string, it will be passed as --name=value. If it's a boolean, it will be
|
||||
// passed as --name if value is true.
|
||||
func Flag(name string, value interface{}) ExecAllocatorOption {
|
||||
return func(p *ExecAllocator) {
|
||||
p.initFlags[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
// UserDataDir is the command line option to set the user data dir.
|
||||
//
|
||||
// Note: set this option to manually set the profile directory used by Chrome.
|
||||
// When this is not set, then a default path will be created in the /tmp
|
||||
// directory.
|
||||
func UserDataDir(dir string) ExecAllocatorOption {
|
||||
return Flag("user-data-dir", dir)
|
||||
}
|
||||
|
||||
// ProxyServer is the command line option to set the outbound proxy server.
|
||||
func ProxyServer(proxy string) ExecAllocatorOption {
|
||||
return Flag("proxy-server", proxy)
|
||||
}
|
||||
|
||||
// WindowSize is the command line option to set the initial window size.
|
||||
func WindowSize(width, height int) ExecAllocatorOption {
|
||||
return Flag("window-size", fmt.Sprintf("%d,%d", width, height))
|
||||
}
|
||||
|
||||
// UserAgent is the command line option to set the default User-Agent
|
||||
// header.
|
||||
func UserAgent(userAgent string) ExecAllocatorOption {
|
||||
return Flag("user-agent", userAgent)
|
||||
}
|
||||
|
||||
// NoSandbox is the Chrome comamnd line option to disable the sandbox.
|
||||
func NoSandbox(p *ExecAllocator) {
|
||||
Flag("no-sandbox", true)(p)
|
||||
}
|
||||
|
||||
// NoFirstRun is the Chrome comamnd line option to disable the first run
|
||||
// dialog.
|
||||
func NoFirstRun(p *ExecAllocator) {
|
||||
Flag("no-first-run", true)(p)
|
||||
}
|
||||
|
||||
// NoDefaultBrowserCheck is the Chrome comamnd line option to disable the
|
||||
// default browser check.
|
||||
func NoDefaultBrowserCheck(p *ExecAllocator) {
|
||||
Flag("no-default-browser-check", true)(p)
|
||||
}
|
||||
|
||||
// Headless is the command line option to run in headless mode.
|
||||
func Headless(p *ExecAllocator) {
|
||||
Flag("headless", true)(p)
|
||||
}
|
||||
|
||||
// DisableGPU is the command line option to disable the GPU process.
|
||||
func DisableGPU(p *ExecAllocator) {
|
||||
Flag("disable-gpu", true)(p)
|
||||
}
|
76
allocate_test.go
Normal file
76
allocate_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExecAllocator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allocCtx, cancel := NewExecAllocator(context.Background(), allocOpts...)
|
||||
defer cancel()
|
||||
|
||||
// TODO: test that multiple child contexts are run in different
|
||||
// processes and browsers.
|
||||
|
||||
taskCtx, cancel := NewContext(allocCtx)
|
||||
defer cancel()
|
||||
|
||||
want := "insert"
|
||||
var got string
|
||||
if err := Run(taskCtx,
|
||||
Navigate(testdataDir+"/form.html"),
|
||||
Text("#foo", &got, ByID),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("wanted %q, got %q", want, got)
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
tempDir := FromContext(taskCtx).Browser.userDataDir
|
||||
if _, err := os.Lstat(tempDir); !os.IsNotExist(err) {
|
||||
t.Fatalf("temporary user data dir %q not deleted", tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecAllocatorCancelParent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allocCtx, allocCancel := NewExecAllocator(context.Background(), allocOpts...)
|
||||
defer allocCancel()
|
||||
|
||||
// TODO: test that multiple child contexts are run in different
|
||||
// processes and browsers.
|
||||
|
||||
taskCtx, _ := NewContext(allocCtx)
|
||||
if err := Run(taskCtx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Canceling the pool context should stop all browsers too.
|
||||
allocCancel()
|
||||
|
||||
tempDir := FromContext(taskCtx).Browser.userDataDir
|
||||
if _, err := os.Lstat(tempDir); !os.IsNotExist(err) {
|
||||
t.Fatalf("temporary user data dir %q not deleted", tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkipNewContext(t *testing.T) {
|
||||
ctx, cancel := NewExecAllocator(context.Background(), allocOpts...)
|
||||
defer cancel()
|
||||
|
||||
// Using the allocator context directly (without calling NewContext)
|
||||
// should be an immediate error.
|
||||
err := Run(ctx, Navigate(testdataDir+"/form.html"))
|
||||
|
||||
want := ErrInvalidContext
|
||||
if err != want {
|
||||
t.Fatalf("want error to be %q, got %q", want, err)
|
||||
}
|
||||
}
|
321
browser.go
Normal file
321
browser.go
Normal file
@ -0,0 +1,321 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/chromedp/cdproto"
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
"github.com/chromedp/cdproto/target"
|
||||
)
|
||||
|
||||
// Browser is the high-level Chrome DevTools Protocol browser manager, handling
|
||||
// the browser process runner, WebSocket clients, associated targets, and
|
||||
// network, page, and DOM events.
|
||||
type Browser struct {
|
||||
conn Transport
|
||||
|
||||
// next is the next message id.
|
||||
next int64
|
||||
|
||||
// tabQueue is the queue used to create new target handlers, once a new
|
||||
// tab is created and attached to. The newly created Target is sent back
|
||||
// via tabResult.
|
||||
tabQueue chan newTab
|
||||
tabResult chan *Target
|
||||
|
||||
// cmdQueue is the outgoing command queue.
|
||||
cmdQueue chan cmdJob
|
||||
|
||||
// logging funcs
|
||||
logf func(string, ...interface{})
|
||||
errf func(string, ...interface{})
|
||||
dbgf func(string, ...interface{})
|
||||
|
||||
// The optional fields below are helpful for some tests.
|
||||
|
||||
// process can be initialized by the allocators which start a process
|
||||
// when allocating a browser.
|
||||
process *os.Process
|
||||
|
||||
// userDataDir can be initialized by the allocators which set up user
|
||||
// data dirs directly.
|
||||
userDataDir string
|
||||
}
|
||||
|
||||
type newTab struct {
|
||||
targetID target.ID
|
||||
sessionID target.SessionID
|
||||
}
|
||||
|
||||
type cmdJob struct {
|
||||
msg *cdproto.Message
|
||||
resp chan *cdproto.Message
|
||||
}
|
||||
|
||||
// NewBrowser creates a new browser.
|
||||
func NewBrowser(ctx context.Context, urlstr string, opts ...BrowserOption) (*Browser, error) {
|
||||
b := &Browser{
|
||||
tabQueue: make(chan newTab, 1),
|
||||
tabResult: make(chan *Target, 1),
|
||||
cmdQueue: make(chan cmdJob),
|
||||
logf: log.Printf,
|
||||
}
|
||||
// apply options
|
||||
for _, o := range opts {
|
||||
o(b)
|
||||
}
|
||||
// ensure errf is set
|
||||
if b.errf == nil {
|
||||
b.errf = func(s string, v ...interface{}) { b.logf("ERROR: "+s, v...) }
|
||||
}
|
||||
|
||||
// dial
|
||||
var err error
|
||||
b.conn, err = DialContext(ctx, ForceIP(urlstr), WithConnDebugf(b.dbgf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go b.run(ctx)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Browser) newExecutorForTarget(ctx context.Context, targetID target.ID, sessionID target.SessionID) *Target {
|
||||
if targetID == "" {
|
||||
panic("empty target ID")
|
||||
}
|
||||
if sessionID == "" {
|
||||
panic("empty session ID")
|
||||
}
|
||||
b.tabQueue <- newTab{targetID, sessionID}
|
||||
return <-b.tabResult
|
||||
}
|
||||
|
||||
func (b *Browser) Execute(ctx context.Context, method string, params json.Marshaler, res json.Unmarshaler) error {
|
||||
paramsMsg := emptyObj
|
||||
if params != nil {
|
||||
var err error
|
||||
if paramsMsg, err = json.Marshal(params); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
id := atomic.AddInt64(&b.next, 1)
|
||||
ch := make(chan *cdproto.Message, 1)
|
||||
b.cmdQueue <- cmdJob{
|
||||
msg: &cdproto.Message{
|
||||
ID: id,
|
||||
Method: cdproto.MethodType(method),
|
||||
Params: paramsMsg,
|
||||
},
|
||||
resp: ch,
|
||||
}
|
||||
select {
|
||||
case msg := <-ch:
|
||||
switch {
|
||||
case msg == nil:
|
||||
return ErrChannelClosed
|
||||
case msg.Error != nil:
|
||||
return msg.Error
|
||||
case res != nil:
|
||||
return json.Unmarshal(msg.Result, res)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type tabEvent struct {
|
||||
sessionID target.SessionID
|
||||
msg *cdproto.Message
|
||||
}
|
||||
|
||||
func (b *Browser) run(ctx context.Context) {
|
||||
defer b.conn.Close()
|
||||
|
||||
cancel := FromContext(ctx).cancel
|
||||
|
||||
// tabEventQueue is the queue of incoming target events, to be routed by
|
||||
// their session ID.
|
||||
tabEventQueue := make(chan tabEvent, 1)
|
||||
|
||||
// resQueue is the incoming command result queue.
|
||||
resQueue := make(chan *cdproto.Message, 1)
|
||||
|
||||
// This goroutine continuously reads events from the websocket
|
||||
// connection. The separate goroutine is needed since a websocket read
|
||||
// is blocking, so it cannot be used in a select statement.
|
||||
go func() {
|
||||
for {
|
||||
msg, err := b.conn.Read()
|
||||
if err != nil {
|
||||
// If the websocket failed, most likely Chrome
|
||||
// was closed or crashed. Cancel the entire
|
||||
// Browser context to stop all activity.
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if msg.Method == cdproto.EventRuntimeExceptionThrown {
|
||||
ev := new(runtime.EventExceptionThrown)
|
||||
if err := json.Unmarshal(msg.Params, ev); err != nil {
|
||||
b.errf("%s", err)
|
||||
continue
|
||||
}
|
||||
b.errf("%+v\n", ev.ExceptionDetails)
|
||||
continue
|
||||
}
|
||||
|
||||
var sessionID target.SessionID
|
||||
if msg.Method == cdproto.EventTargetReceivedMessageFromTarget {
|
||||
event := new(target.EventReceivedMessageFromTarget)
|
||||
if err := json.Unmarshal(msg.Params, event); err != nil {
|
||||
b.errf("%s", err)
|
||||
continue
|
||||
}
|
||||
sessionID = event.SessionID
|
||||
msg = new(cdproto.Message)
|
||||
if err := json.Unmarshal([]byte(event.Message), msg); err != nil {
|
||||
b.errf("%s", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case msg.Method != "":
|
||||
if sessionID == "" {
|
||||
// TODO: are we interested in browser events?
|
||||
continue
|
||||
}
|
||||
tabEventQueue <- tabEvent{
|
||||
sessionID: sessionID,
|
||||
msg: msg,
|
||||
}
|
||||
case msg.ID != 0:
|
||||
// We can't process the response here, as it's
|
||||
// another goroutine that maintans respByID.
|
||||
resQueue <- msg
|
||||
default:
|
||||
b.errf("ignoring malformed incoming message (missing id or method): %#v", msg)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// This goroutine handles tabs, as well as routing events to each tab
|
||||
// via the pages map.
|
||||
go func() {
|
||||
// This map is only safe for use within this goroutine, so don't
|
||||
// declare it as a Browser field.
|
||||
pages := make(map[target.SessionID]*Target, 1024)
|
||||
for {
|
||||
select {
|
||||
case tab := <-b.tabQueue:
|
||||
if _, ok := pages[tab.sessionID]; ok {
|
||||
b.errf("executor for %q already exists", tab.sessionID)
|
||||
}
|
||||
t := &Target{
|
||||
browser: b,
|
||||
TargetID: tab.targetID,
|
||||
SessionID: tab.sessionID,
|
||||
|
||||
eventQueue: make(chan *cdproto.Message, 1024),
|
||||
waitQueue: make(chan func(cur *cdp.Frame) bool, 1024),
|
||||
frames: make(map[cdp.FrameID]*cdp.Frame),
|
||||
|
||||
logf: b.logf,
|
||||
errf: b.errf,
|
||||
}
|
||||
go t.run(ctx)
|
||||
pages[tab.sessionID] = t
|
||||
b.tabResult <- t
|
||||
case event := <-tabEventQueue:
|
||||
page, ok := pages[event.sessionID]
|
||||
if !ok {
|
||||
b.errf("unknown session ID %q", event.sessionID)
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case page.eventQueue <- event.msg:
|
||||
default:
|
||||
panic("eventQueue is full")
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
respByID := make(map[int64]chan *cdproto.Message)
|
||||
|
||||
// This goroutine handles sending commands to the browser, and sending
|
||||
// responses back for each of these commands via respByID.
|
||||
for {
|
||||
select {
|
||||
case res := <-resQueue:
|
||||
resp, ok := respByID[res.ID]
|
||||
if !ok {
|
||||
b.errf("id %d not present in response map", res.ID)
|
||||
continue
|
||||
}
|
||||
if resp != nil {
|
||||
// resp could be nil, if we're not interested in
|
||||
// this response; for CommandSendMessageToTarget.
|
||||
resp <- res
|
||||
close(resp)
|
||||
}
|
||||
delete(respByID, res.ID)
|
||||
|
||||
case q := <-b.cmdQueue:
|
||||
if _, ok := respByID[q.msg.ID]; ok {
|
||||
b.errf("id %d already present in response map", q.msg.ID)
|
||||
continue
|
||||
}
|
||||
respByID[q.msg.ID] = q.resp
|
||||
|
||||
if q.msg.Method == "" {
|
||||
// Only register the chananel in respByID;
|
||||
// useful for CommandSendMessageToTarget.
|
||||
continue
|
||||
}
|
||||
if err := b.conn.Write(q.msg); err != nil {
|
||||
b.errf("%s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BrowserOption is a browser option.
|
||||
type BrowserOption func(*Browser)
|
||||
|
||||
// WithBrowserLogf is a browser option to specify a func to receive general logging.
|
||||
func WithBrowserLogf(f func(string, ...interface{})) BrowserOption {
|
||||
return func(b *Browser) { b.logf = f }
|
||||
}
|
||||
|
||||
// WithBrowserErrorf is a browser option to specify a func to receive error logging.
|
||||
func WithBrowserErrorf(f func(string, ...interface{})) BrowserOption {
|
||||
return func(b *Browser) { b.errf = f }
|
||||
}
|
||||
|
||||
// WithBrowserDebugf is a browser option to specify a func to log actual
|
||||
// websocket messages.
|
||||
func WithBrowserDebugf(f func(string, ...interface{})) BrowserOption {
|
||||
return func(b *Browser) { b.dbgf = f }
|
||||
}
|
||||
|
||||
// WithConsolef is a browser option to specify a func to receive chrome log events.
|
||||
//
|
||||
// Note: NOT YET IMPLEMENTED.
|
||||
func WithConsolef(f func(string, ...interface{})) BrowserOption {
|
||||
return func(b *Browser) {
|
||||
}
|
||||
}
|
603
chromedp.go
603
chromedp.go
@ -8,428 +8,285 @@ package chromedp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
|
||||
"github.com/chromedp/chromedp/client"
|
||||
"github.com/chromedp/chromedp/runner"
|
||||
"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/cdproto/target"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultNewTargetTimeout is the default time to wait for a new target to
|
||||
// be started.
|
||||
DefaultNewTargetTimeout = 3 * time.Second
|
||||
// Context is attached to any context.Context which is valid for use with Run.
|
||||
type Context struct {
|
||||
// Allocator is used to create new browsers. It is inherited from the
|
||||
// parent context when using NewContext.
|
||||
Allocator Allocator
|
||||
|
||||
// DefaultCheckDuration is the default time to sleep between a check.
|
||||
DefaultCheckDuration = 50 * time.Millisecond
|
||||
// Browser is the browser being used in the context. It is inherited
|
||||
// from the parent context when using NewContext.
|
||||
Browser *Browser
|
||||
|
||||
// DefaultPoolStartPort is the default start port number.
|
||||
DefaultPoolStartPort = 9000
|
||||
// Target is the target to run actions (commands) against. It is not
|
||||
// inherited from the parent context, and typically each context will
|
||||
// have its own unique Target pointing to a separate browser tab (page).
|
||||
Target *Target
|
||||
|
||||
// DefaultPoolEndPort is the default end port number.
|
||||
DefaultPoolEndPort = 10000
|
||||
)
|
||||
// browserOpts holds the browser options passed to NewContext via
|
||||
// WithBrowserOption, so that they can later be used when allocating a
|
||||
// browser in Run.
|
||||
browserOpts []BrowserOption
|
||||
|
||||
// CDP is the high-level Chrome DevTools Protocol browser manager, handling the
|
||||
// browser process runner, WebSocket clients, associated targets, and network,
|
||||
// page, and DOM events.
|
||||
type CDP struct {
|
||||
// r is the chrome runner.
|
||||
r *runner.Runner
|
||||
// cancel simply cancels the context that was used to start Browser.
|
||||
// This is useful to stop all activity and avoid deadlocks if we detect
|
||||
// that the browser was closed or happened to crash. Note that this
|
||||
// cancel function doesn't do any waiting.
|
||||
cancel func()
|
||||
|
||||
// opts are command line options to pass to a created runner.
|
||||
opts []runner.CommandLineOption
|
||||
// first records whether this context was the one that allocated
|
||||
// Browser. This is important, because its cancellation will stop the
|
||||
// entire browser handler, meaning that no further actions can be
|
||||
// executed.
|
||||
first bool
|
||||
|
||||
// watch is the channel for new client targets.
|
||||
watch <-chan client.Target
|
||||
// wg allows waiting for a target to be closed on cancellation.
|
||||
wg sync.WaitGroup
|
||||
|
||||
// cur is the current active target's handler.
|
||||
cur cdp.Executor
|
||||
|
||||
// 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
|
||||
// cancelErr is the first error encountered when cancelling this
|
||||
// context, for example if a browser's temporary user data directory
|
||||
// couldn't be deleted.
|
||||
cancelErr error
|
||||
}
|
||||
|
||||
// New creates and starts a new CDP instance.
|
||||
func New(ctxt context.Context, opts ...Option) (*CDP, error) {
|
||||
c := &CDP{
|
||||
handlers: make([]*TargetHandler, 0),
|
||||
handlerMap: make(map[string]int),
|
||||
logf: log.Printf,
|
||||
debugf: func(string, ...interface{}) {},
|
||||
errf: func(s string, v ...interface{}) { log.Printf("error: "+s, v...) },
|
||||
// NewContext creates a chromedp context from the parent context. The parent
|
||||
// context's Allocator is inherited, defaulting to an ExecAllocator with
|
||||
// DefaultExecAllocatorOptions.
|
||||
//
|
||||
// If the parent context contains an allocated Browser, the child context
|
||||
// inherits it, and its first Run creates a new tab on that browser. Otherwise,
|
||||
// its first Run will allocate a new browser.
|
||||
//
|
||||
// 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 {
|
||||
if err := o(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// check for supplied runner, if none then create one
|
||||
if c.r == nil && c.watch == nil {
|
||||
var err error
|
||||
c.r, err = runner.Run(ctxt, c.opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// watch handlers
|
||||
if c.watch == nil {
|
||||
c.watch = c.r.Client().WatchPageTargets(ctxt)
|
||||
o(c)
|
||||
}
|
||||
if c.Allocator == nil {
|
||||
c.Allocator = setupExecAllocator(DefaultExecAllocatorOptions...)
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, contextKey{}, c)
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
for t := range c.watch {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
go c.AddTarget(ctxt, t)
|
||||
<-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()
|
||||
}()
|
||||
|
||||
// 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")
|
||||
}
|
||||
cancelWait := func() {
|
||||
cancel()
|
||||
c.wg.Wait()
|
||||
}
|
||||
return ctx, cancelWait
|
||||
}
|
||||
|
||||
// AddTarget adds a target using the supplied context.
|
||||
func (c *CDP) AddTarget(ctxt context.Context, t client.Target) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
type contextKey struct{}
|
||||
|
||||
// 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
|
||||
}
|
||||
// FromContext extracts the Context data stored inside a context.Context.
|
||||
func FromContext(ctx context.Context) *Context {
|
||||
c, _ := ctx.Value(contextKey{}).(*Context)
|
||||
return c
|
||||
}
|
||||
|
||||
// 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()
|
||||
// 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
|
||||
}
|
||||
|
||||
return nil
|
||||
c.cancel()
|
||||
c.wg.Wait()
|
||||
return c.cancelErr
|
||||
}
|
||||
|
||||
// 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...)
|
||||
// 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
|
||||
}
|
||||
|
||||
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 c.Browser == nil {
|
||||
browser, err := c.Allocator.Allocate(ctx, c.browserOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if id != nil {
|
||||
*id = n
|
||||
c.Browser = browser
|
||||
}
|
||||
if c.Target == nil {
|
||||
if err := c.newSession(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return Tasks(actions).Do(ctx, c.Target)
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
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 == "" {
|
||||
var err error
|
||||
targetID, err = target.CreateTarget("about:blank").Do(ctx, c.Browser)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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)
|
||||
// WithErrorf is a shortcut for WithBrowserOption(WithBrowserErrorf(f)).
|
||||
func WithErrorf(f func(string, ...interface{})) ContextOption {
|
||||
return WithBrowserOption(WithBrowserErrorf(f))
|
||||
}
|
||||
|
||||
// Option is a Chrome DevTools Protocol option.
|
||||
type Option func(*CDP) error
|
||||
// WithDebugf is a shortcut for WithBrowserOption(WithBrowserDebugf(f)).
|
||||
func WithDebugf(f func(string, ...interface{})) ContextOption {
|
||||
return WithBrowserOption(WithBrowserDebugf(f))
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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...)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if c.Browser == nil {
|
||||
browser, err := c.Allocator.Allocate(ctx, c.browserOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Browser = browser
|
||||
}
|
||||
return target.GetTargets().Do(ctx, c.Browser)
|
||||
}
|
||||
|
||||
// WithURL is a CDP option to use a client with the specified URL.
|
||||
func WithURL(ctxt context.Context, urlstr string) Option {
|
||||
return func(c *CDP) error {
|
||||
return WithClient(ctxt, client.New(client.URL(urlstr)))(c)
|
||||
}
|
||||
}
|
||||
|
||||
// WithRunnerOptions is a CDP option to specify the options to pass to a newly
|
||||
// created Chrome process runner.
|
||||
func WithRunnerOptions(opts ...runner.CommandLineOption) Option {
|
||||
return func(c *CDP) error {
|
||||
c.opts = opts
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogf is a CDP option to specify a func to receive general logging.
|
||||
func WithLogf(f func(string, ...interface{})) Option {
|
||||
return func(c *CDP) error {
|
||||
c.logf = f
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDebugf is a CDP option to specify a func to receive debug logging (ie,
|
||||
// protocol information).
|
||||
func WithDebugf(f func(string, ...interface{})) Option {
|
||||
return func(c *CDP) error {
|
||||
c.debugf = f
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithErrorf is a CDP option to specify a func to receive error logging.
|
||||
func WithErrorf(f func(string, ...interface{})) Option {
|
||||
return func(c *CDP) error {
|
||||
c.errf = f
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithLog is a CDP option that sets the logging, debugging, and error funcs to
|
||||
// f.
|
||||
func WithLog(f func(string, ...interface{})) Option {
|
||||
return func(c *CDP) error {
|
||||
c.logf, c.debugf, c.errf = f, f, f
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithConsolef is a CDP option to specify a func to receive chrome log events.
|
||||
//
|
||||
// Note: NOT YET IMPLEMENTED.
|
||||
func WithConsolef(f func(string, ...interface{})) Option {
|
||||
return func(c *CDP) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// defaultNewTargetTimeout is the default target timeout -- used by
|
||||
// testing.
|
||||
defaultNewTargetTimeout = DefaultNewTargetTimeout
|
||||
)
|
||||
|
263
chromedp_test.go
263
chromedp_test.go
@ -2,117 +2,226 @@ package chromedp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp/runner"
|
||||
)
|
||||
|
||||
var (
|
||||
pool *Pool
|
||||
testdataDir string
|
||||
|
||||
defaultContext, defaultCancel = context.WithCancel(context.Background())
|
||||
browserCtx context.Context
|
||||
|
||||
cliOpts = []runner.CommandLineOption{
|
||||
runner.NoDefaultBrowserCheck,
|
||||
runner.NoFirstRun,
|
||||
}
|
||||
// allocOpts is filled in TestMain
|
||||
allocOpts []ExecAllocatorOption
|
||||
)
|
||||
|
||||
func testAllocate(t *testing.T, path string) *Res {
|
||||
c, err := pool.Allocate(defaultContext, cliOpts...)
|
||||
if err != nil {
|
||||
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...)
|
||||
}
|
||||
func testAllocate(t *testing.T, path string) (_ context.Context, cancel func()) {
|
||||
// Same browser, new tab; not needing to start new chrome browsers for
|
||||
// each test gives a huge speed-up.
|
||||
ctx, _ := NewContext(browserCtx)
|
||||
|
||||
// Only navigate if we want a path, otherwise leave the blank page.
|
||||
if path != "" {
|
||||
err = c.Run(defaultContext, Navigate(testdataDir+"/"+path))
|
||||
if err != nil {
|
||||
t.Fatalf("could not navigate to testdata/%s: %v", path, err)
|
||||
if err := Run(ctx, Navigate(testdataDir+"/"+path)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
cancelErr := func() {
|
||||
if err := Cancel(ctx); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
return ctx, cancelErr
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var err error
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("could not get working directory: %v", err)
|
||||
os.Exit(1)
|
||||
panic(fmt.Sprintf("could not get working directory: %v", err))
|
||||
}
|
||||
testdataDir = "file://" + path.Join(wd, "testdata")
|
||||
|
||||
// its worth noting that newer versions of chrome (64+) run much faster
|
||||
// than older ones -- same for headless_shell ...
|
||||
execPath := os.Getenv("CHROMEDP_TEST_RUNNER")
|
||||
if execPath == "" {
|
||||
execPath = runner.LookChromeNames("headless_shell")
|
||||
}
|
||||
cliOpts = append(cliOpts, runner.ExecPath(execPath))
|
||||
// build on top of the default options
|
||||
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 ...
|
||||
if execPath := os.Getenv("CHROMEDP_TEST_RUNNER"); execPath != "" {
|
||||
allocOpts = append(allocOpts, ExecPath(execPath))
|
||||
}
|
||||
// not explicitly needed to be set, as this vastly speeds up unit tests
|
||||
if noSandbox := os.Getenv("CHROMEDP_NO_SANDBOX"); noSandbox != "false" {
|
||||
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)
|
||||
allocOpts = append(allocOpts, NoSandbox)
|
||||
}
|
||||
|
||||
if targetTimeout := os.Getenv("CHROMEDP_TARGET_TIMEOUT"); targetTimeout != "" {
|
||||
defaultNewTargetTimeout, _ = time.ParseDuration(targetTimeout)
|
||||
}
|
||||
if defaultNewTargetTimeout == 0 {
|
||||
defaultNewTargetTimeout = 30 * time.Second
|
||||
}
|
||||
allocCtx, cancel := NewExecAllocator(context.Background(), allocOpts...)
|
||||
|
||||
//pool, err = NewPool(PoolLog(log.Printf, log.Printf, log.Printf))
|
||||
pool, err = NewPool()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
// start the browser
|
||||
browserCtx, _ = NewContext(allocCtx)
|
||||
if err := Run(browserCtx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
defaultCancel()
|
||||
|
||||
err = pool.Shutdown()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestTargets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Start one browser with one tab.
|
||||
ctx1, cancel1 := NewContext(context.Background())
|
||||
defer cancel1()
|
||||
if err := Run(ctx1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantTargets := func(ctx context.Context, want int) {
|
||||
t.Helper()
|
||||
infos, err := Targets(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := len(infos); want != got {
|
||||
t.Fatalf("want %d targets, got %d", want, got)
|
||||
}
|
||||
}
|
||||
wantTargets(ctx1, 1)
|
||||
|
||||
// Start a second tab on the same browser.
|
||||
ctx2, cancel2 := NewContext(ctx1)
|
||||
defer cancel2()
|
||||
if err := Run(ctx2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantTargets(ctx2, 2)
|
||||
|
||||
// The first context should also see both targets.
|
||||
wantTargets(ctx1, 2)
|
||||
|
||||
// Cancelling the second context should close the second tab alone.
|
||||
cancel2()
|
||||
wantTargets(ctx1, 1)
|
||||
|
||||
// We used to have a bug where Run would reset the first context as if
|
||||
// it weren't the first, breaking its cancellation.
|
||||
if err := Run(ctx1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowserQuit(t *testing.T) {
|
||||
t.Parallel()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("os.Interrupt isn't supported on Windows")
|
||||
}
|
||||
|
||||
// Simulate a scenario where we navigate to a page that's slow to
|
||||
// respond, and the browser is closed before we can finish the
|
||||
// navigation.
|
||||
serve := make(chan bool, 1)
|
||||
close := make(chan bool, 1)
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
close <- true
|
||||
<-serve
|
||||
fmt.Fprintf(w, "response")
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ctx, cancel := NewContext(context.Background())
|
||||
defer cancel()
|
||||
if err := Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-close
|
||||
b := FromContext(ctx).Browser
|
||||
if err := b.process.Signal(os.Interrupt); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
serve <- true
|
||||
}()
|
||||
|
||||
// Run should error with something other than "deadline exceeded" in
|
||||
// much less than 5s.
|
||||
ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
switch err := Run(ctx2, Navigate(s.URL)); err {
|
||||
case nil:
|
||||
t.Fatal("did not expect a nil error")
|
||||
case context.DeadlineExceeded:
|
||||
t.Fatalf("did not expect a standard context error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx1, cancel1 := NewContext(context.Background())
|
||||
defer cancel1()
|
||||
if err := Run(ctx1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Open and close a target normally; no error.
|
||||
ctx2, cancel2 := NewContext(ctx1)
|
||||
defer cancel2()
|
||||
if err := Run(ctx2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Cancel(ctx2); err != nil {
|
||||
t.Fatalf("expected a nil error, got %v", err)
|
||||
}
|
||||
|
||||
// Make "cancel" close the wrong target; error.
|
||||
ctx3, cancel3 := NewContext(ctx1)
|
||||
defer cancel3()
|
||||
if err := Run(ctx3); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
FromContext(ctx3).Target.TargetID = "wrong"
|
||||
if err := Cancel(ctx3); err == nil {
|
||||
t.Fatalf("expected a non-nil error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrematureCancel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Cancel before the browser is allocated.
|
||||
ctx, cancel := NewContext(context.Background())
|
||||
cancel()
|
||||
if err := Run(ctx); err != context.Canceled {
|
||||
t.Fatalf("wanted canceled context error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrematureCancelTab(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx1, cancel := NewContext(context.Background())
|
||||
defer cancel()
|
||||
if err := Run(ctx1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Cancel after the browser is allocated, but before we've created a new
|
||||
// tab.
|
||||
ctx2, cancel := NewContext(ctx1)
|
||||
cancel()
|
||||
Run(ctx2)
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
package client
|
||||
|
||||
import "fmt"
|
||||
|
||||
//go:generate easyjson -omit_empty -output_filename easyjson.go chrome.go
|
||||
|
||||
// Chrome holds connection information for a Chrome target.
|
||||
//
|
||||
//easyjson:json
|
||||
type Chrome struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
DevtoolsURL string `json:"devtoolsFrontendUrl,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Type TargetType `json:"type,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
WebsocketURL string `json:"webSocketDebuggerUrl,omitempty"`
|
||||
FaviconURL string `json:"faviconURL,omitempty"`
|
||||
}
|
||||
|
||||
// String satisfies the stringer interface.
|
||||
func (c Chrome) String() string {
|
||||
return fmt.Sprintf("[%s]: %q", c.ID, c.Title)
|
||||
}
|
||||
|
||||
// GetID returns the target ID.
|
||||
func (c *Chrome) GetID() string {
|
||||
return c.ID
|
||||
}
|
||||
|
||||
// GetType returns the target type.
|
||||
func (c *Chrome) GetType() TargetType {
|
||||
return c.Type
|
||||
}
|
||||
|
||||
// GetDevtoolsURL returns the devtools frontend target URL, satisfying the
|
||||
// domains.Target interface.
|
||||
func (c *Chrome) GetDevtoolsURL() string {
|
||||
return c.DevtoolsURL
|
||||
}
|
||||
|
||||
// GetWebsocketURL provides the websocket URL for the target, satisfying the
|
||||
// domains.Target interface.
|
||||
func (c *Chrome) GetWebsocketURL() string {
|
||||
return c.WebsocketURL
|
||||
}
|
340
client/client.go
340
client/client.go
@ -1,340 +0,0 @@
|
||||
// Package client provides the low level Chrome DevTools Protocol client.
|
||||
package client
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultEndpoint is the default endpoint to connect to.
|
||||
DefaultEndpoint = "http://localhost:9222/json"
|
||||
|
||||
// DefaultWatchInterval is the default check duration.
|
||||
DefaultWatchInterval = 100 * time.Millisecond
|
||||
|
||||
// DefaultWatchTimeout is the default watch timeout.
|
||||
DefaultWatchTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// Error is a client error.
|
||||
type Error string
|
||||
|
||||
// Error satisfies the error interface.
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrUnsupportedProtocolType is the unsupported protocol type error.
|
||||
ErrUnsupportedProtocolType Error = "unsupported protocol type"
|
||||
|
||||
// ErrUnsupportedProtocolVersion is the unsupported protocol version error.
|
||||
ErrUnsupportedProtocolVersion Error = "unsupported protocol version"
|
||||
)
|
||||
|
||||
// Target is the common interface for a Chrome DevTools Protocol target.
|
||||
type Target interface {
|
||||
String() string
|
||||
GetID() string
|
||||
GetType() TargetType
|
||||
GetDevtoolsURL() string
|
||||
GetWebsocketURL() string
|
||||
}
|
||||
|
||||
// Client is a Chrome DevTools Protocol client.
|
||||
type Client struct {
|
||||
url string
|
||||
check time.Duration
|
||||
timeout time.Duration
|
||||
|
||||
ver, typ string
|
||||
rw sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates a new Chrome DevTools Protocol client.
|
||||
func New(opts ...Option) *Client {
|
||||
c := &Client{
|
||||
url: DefaultEndpoint,
|
||||
check: DefaultWatchInterval,
|
||||
timeout: DefaultWatchTimeout,
|
||||
}
|
||||
|
||||
// apply opts
|
||||
for _, o := range opts {
|
||||
o(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// doReq executes a request.
|
||||
func (c *Client) doReq(ctxt context.Context, action string, v interface{}) error {
|
||||
// create request
|
||||
req, err := http.NewRequest("GET", c.url+"/"+action, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctxt)
|
||||
|
||||
cl := &http.Client{}
|
||||
|
||||
// execute
|
||||
res, err := cl.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if v != nil {
|
||||
// load body
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// unmarshal
|
||||
if z, ok := v.(easyjson.Unmarshaler); ok {
|
||||
return easyjson.Unmarshal(body, z)
|
||||
}
|
||||
|
||||
return json.Unmarshal(body, v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListTargets returns a list of all targets.
|
||||
func (c *Client) ListTargets(ctxt context.Context) ([]Target, error) {
|
||||
var err error
|
||||
|
||||
var l []json.RawMessage
|
||||
if err = c.doReq(ctxt, "list", &l); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := make([]Target, len(l))
|
||||
for i, v := range l {
|
||||
t[i], err = c.newTarget(ctxt, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ListTargetsWithType returns a list of Targets with the specified target
|
||||
// type.
|
||||
func (c *Client) ListTargetsWithType(ctxt context.Context, typ TargetType) ([]Target, error) {
|
||||
var err error
|
||||
|
||||
targets, err := c.ListTargets(ctxt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []Target
|
||||
for _, t := range targets {
|
||||
if t.GetType() == typ {
|
||||
ret = append(ret, t)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// ListPageTargets lists the available Page targets.
|
||||
func (c *Client) ListPageTargets(ctxt context.Context) ([]Target, error) {
|
||||
return c.ListTargetsWithType(ctxt, Page)
|
||||
}
|
||||
|
||||
var browserRE = regexp.MustCompile(`(?i)^(chrome|chromium|microsoft edge|safari)`)
|
||||
|
||||
// loadProtocolInfo loads the protocol information from the remote URL.
|
||||
func (c *Client) loadProtocolInfo(ctxt context.Context) (string, string, error) {
|
||||
c.rw.Lock()
|
||||
defer c.rw.Unlock()
|
||||
|
||||
if c.ver == "" || c.typ == "" {
|
||||
v, err := c.VersionInfo(ctxt)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if m := browserRE.FindAllStringSubmatch(v["Browser"], -1); len(m) != 0 {
|
||||
c.typ = strings.ToLower(m[0][0])
|
||||
}
|
||||
c.ver = v["Protocol-Version"]
|
||||
}
|
||||
|
||||
return c.ver, c.typ, nil
|
||||
}
|
||||
|
||||
// newTarget creates a new target.
|
||||
func (c *Client) newTarget(ctxt context.Context, buf []byte) (Target, error) {
|
||||
var err error
|
||||
|
||||
ver, typ, err := c.loadProtocolInfo(ctxt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ver != "1.1" && ver != "1.2" && ver != "1.3" {
|
||||
return nil, ErrUnsupportedProtocolVersion
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case "chrome", "chromium", "microsoft edge", "safari", "":
|
||||
x := new(Chrome)
|
||||
if buf != nil {
|
||||
if err = easyjson.Unmarshal(buf, x); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return x, nil
|
||||
}
|
||||
|
||||
return nil, ErrUnsupportedProtocolType
|
||||
}
|
||||
|
||||
// NewPageTargetWithURL creates a new page target with the specified url.
|
||||
func (c *Client) NewPageTargetWithURL(ctxt context.Context, urlstr string) (Target, error) {
|
||||
var err error
|
||||
|
||||
t, err := c.newTarget(ctxt, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := "new"
|
||||
if urlstr != "" {
|
||||
u += "?" + urlstr
|
||||
}
|
||||
|
||||
if err = c.doReq(ctxt, u, t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// NewPageTarget creates a new page target.
|
||||
func (c *Client) NewPageTarget(ctxt context.Context) (Target, error) {
|
||||
return c.NewPageTargetWithURL(ctxt, "")
|
||||
}
|
||||
|
||||
// ActivateTarget activates a target.
|
||||
func (c *Client) ActivateTarget(ctxt context.Context, t Target) error {
|
||||
return c.doReq(ctxt, "activate/"+t.GetID(), nil)
|
||||
}
|
||||
|
||||
// CloseTarget activates a target.
|
||||
func (c *Client) CloseTarget(ctxt context.Context, t Target) error {
|
||||
return c.doReq(ctxt, "close/"+t.GetID(), nil)
|
||||
}
|
||||
|
||||
// VersionInfo returns information about the remote debugging protocol.
|
||||
func (c *Client) VersionInfo(ctxt context.Context) (map[string]string, error) {
|
||||
v := make(map[string]string)
|
||||
if err := c.doReq(ctxt, "version", &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// WatchPageTargets watches for new page targets.
|
||||
func (c *Client) WatchPageTargets(ctxt context.Context) <-chan Target {
|
||||
ch := make(chan Target)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
encountered := make(map[string]bool)
|
||||
check := func() error {
|
||||
targets, err := c.ListPageTargets(ctxt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range targets {
|
||||
if !encountered[t.GetID()] {
|
||||
ch <- t
|
||||
}
|
||||
encountered[t.GetID()] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
lastGood := time.Now()
|
||||
for {
|
||||
err = check()
|
||||
if err == nil {
|
||||
lastGood = time.Now()
|
||||
} else if time.Now().After(lastGood.Add(c.timeout)) {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(c.check):
|
||||
continue
|
||||
|
||||
case <-ctxt.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// Option is a Chrome DevTools Protocol client option.
|
||||
type Option func(*Client)
|
||||
|
||||
// URL is a client option to specify the remote Chrome DevTools Protocol
|
||||
// instance to connect to.
|
||||
func URL(urlstr string) Option {
|
||||
return func(c *Client) {
|
||||
// since chrome 66+, dev tools requires the host name to be either an
|
||||
// IP address, or "localhost"
|
||||
if strings.HasPrefix(strings.ToLower(urlstr), "http://") {
|
||||
host, port, path := urlstr[7:], "", ""
|
||||
if i := strings.Index(host, "/"); i != -1 {
|
||||
host, path = host[:i], host[i:]
|
||||
}
|
||||
if i := strings.Index(host, ":"); i != -1 {
|
||||
host, port = host[:i], host[i:]
|
||||
}
|
||||
if addr, err := net.ResolveIPAddr("ip", host); err == nil {
|
||||
urlstr = "http://" + addr.IP.String() + port + path
|
||||
}
|
||||
}
|
||||
c.url = urlstr
|
||||
}
|
||||
}
|
||||
|
||||
// WatchInterval is a client option that specifies the check interval duration.
|
||||
func WatchInterval(check time.Duration) Option {
|
||||
return func(c *Client) {
|
||||
c.check = check
|
||||
}
|
||||
}
|
||||
|
||||
// WatchTimeout is a client option that specifies the watch timeout duration.
|
||||
func WatchTimeout(timeout time.Duration) Option {
|
||||
return func(c *Client) {
|
||||
c.timeout = timeout
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
json "encoding/json"
|
||||
easyjson "github.com/mailru/easyjson"
|
||||
jlexer "github.com/mailru/easyjson/jlexer"
|
||||
jwriter "github.com/mailru/easyjson/jwriter"
|
||||
)
|
||||
|
||||
// suppress unused package warning
|
||||
var (
|
||||
_ *json.RawMessage
|
||||
_ *jlexer.Lexer
|
||||
_ *jwriter.Writer
|
||||
_ easyjson.Marshaler
|
||||
)
|
||||
|
||||
func easyjsonC5a4559bDecodeGithubComChromedpChromedpClient(in *jlexer.Lexer, out *Chrome) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
in.Skip()
|
||||
return
|
||||
}
|
||||
in.Delim('{')
|
||||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeString()
|
||||
in.WantColon()
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
in.WantComma()
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "description":
|
||||
out.Description = string(in.String())
|
||||
case "devtoolsFrontendUrl":
|
||||
out.DevtoolsURL = string(in.String())
|
||||
case "id":
|
||||
out.ID = string(in.String())
|
||||
case "title":
|
||||
out.Title = string(in.String())
|
||||
case "type":
|
||||
(out.Type).UnmarshalEasyJSON(in)
|
||||
case "url":
|
||||
out.URL = string(in.String())
|
||||
case "webSocketDebuggerUrl":
|
||||
out.WebsocketURL = string(in.String())
|
||||
case "faviconURL":
|
||||
out.FaviconURL = string(in.String())
|
||||
default:
|
||||
in.SkipRecursive()
|
||||
}
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim('}')
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjsonC5a4559bEncodeGithubComChromedpChromedpClient(out *jwriter.Writer, in Chrome) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
if in.Description != "" {
|
||||
const prefix string = ",\"description\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.Description))
|
||||
}
|
||||
if in.DevtoolsURL != "" {
|
||||
const prefix string = ",\"devtoolsFrontendUrl\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.DevtoolsURL))
|
||||
}
|
||||
if in.ID != "" {
|
||||
const prefix string = ",\"id\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.ID))
|
||||
}
|
||||
if in.Title != "" {
|
||||
const prefix string = ",\"title\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.Title))
|
||||
}
|
||||
if in.Type != "" {
|
||||
const prefix string = ",\"type\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
(in.Type).MarshalEasyJSON(out)
|
||||
}
|
||||
if in.URL != "" {
|
||||
const prefix string = ",\"url\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.URL))
|
||||
}
|
||||
if in.WebsocketURL != "" {
|
||||
const prefix string = ",\"webSocketDebuggerUrl\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.WebsocketURL))
|
||||
}
|
||||
if in.FaviconURL != "" {
|
||||
const prefix string = ",\"faviconURL\":"
|
||||
if first {
|
||||
first = false
|
||||
out.RawString(prefix[1:])
|
||||
} else {
|
||||
out.RawString(prefix)
|
||||
}
|
||||
out.String(string(in.FaviconURL))
|
||||
}
|
||||
out.RawByte('}')
|
||||
}
|
||||
|
||||
// MarshalJSON supports json.Marshaler interface
|
||||
func (v Chrome) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
easyjsonC5a4559bEncodeGithubComChromedpChromedpClient(&w, v)
|
||||
return w.Buffer.BuildBytes(), w.Error
|
||||
}
|
||||
|
||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||
func (v Chrome) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
easyjsonC5a4559bEncodeGithubComChromedpChromedpClient(w, v)
|
||||
}
|
||||
|
||||
// UnmarshalJSON supports json.Unmarshaler interface
|
||||
func (v *Chrome) UnmarshalJSON(data []byte) error {
|
||||
r := jlexer.Lexer{Data: data}
|
||||
easyjsonC5a4559bDecodeGithubComChromedpChromedpClient(&r, v)
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||
func (v *Chrome) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
easyjsonC5a4559bDecodeGithubComChromedpChromedpClient(l, v)
|
||||
}
|
145
client/gen.go
145
client/gen.go
@ -1,145 +0,0 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/knq/snaker"
|
||||
)
|
||||
|
||||
const (
|
||||
// chromiumSrc is the base chromium source repo location
|
||||
chromiumSrc = "https://chromium.googlesource.com/chromium/src"
|
||||
|
||||
// devtoolsHTTPClientCc contains the target_type names.
|
||||
devtoolsHTTPClientCc = chromiumSrc + "/+/master/chrome/test/chromedriver/chrome/devtools_http_client.cc?format=TEXT"
|
||||
)
|
||||
|
||||
var (
|
||||
flagOut = flag.String("out", "targettype.go", "out file")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if err := run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var typeAsStringRE = regexp.MustCompile(`type_as_string\s+==\s+"([^"]+)"`)
|
||||
|
||||
// run executes the generator.
|
||||
func run() error {
|
||||
var err error
|
||||
|
||||
// grab source
|
||||
buf, err := grab(devtoolsHTTPClientCc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find names
|
||||
matches := typeAsStringRE.FindAllStringSubmatch(string(buf), -1)
|
||||
names := make([]string, len(matches))
|
||||
for i, m := range matches {
|
||||
names[i] = m[1]
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
// process names
|
||||
var constVals, decodeVals string
|
||||
for _, n := range names {
|
||||
name := snaker.SnakeToCamelIdentifier(n)
|
||||
constVals += fmt.Sprintf("%s TargetType = \"%s\"\n", name, n)
|
||||
decodeVals += fmt.Sprintf("case %s:\n*tt=%s\n", name, name)
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(*flagOut, []byte(fmt.Sprintf(targetTypeSrc, constVals, decodeVals)), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return exec.Command("gofmt", "-w", "-s", *flagOut).Run()
|
||||
}
|
||||
|
||||
// grab retrieves a file from the chromium source code.
|
||||
func grab(path string) ([]byte, error) {
|
||||
res, err := http.Get(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf, err := base64.StdEncoding.DecodeString(string(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
const (
|
||||
targetTypeSrc = `package client
|
||||
|
||||
// Code generated by gen.go. DO NOT EDIT.
|
||||
|
||||
import (
|
||||
easyjson "github.com/mailru/easyjson"
|
||||
jlexer "github.com/mailru/easyjson/jlexer"
|
||||
jwriter "github.com/mailru/easyjson/jwriter"
|
||||
)
|
||||
|
||||
// TargetType are the types of targets available in Chrome.
|
||||
type TargetType string
|
||||
|
||||
// TargetType values.
|
||||
const (
|
||||
%s
|
||||
)
|
||||
|
||||
// String satisfies stringer.
|
||||
func (tt TargetType) String() string {
|
||||
return string(tt)
|
||||
}
|
||||
|
||||
// MarshalEasyJSON satisfies easyjson.Marshaler.
|
||||
func (tt TargetType) MarshalEasyJSON(out *jwriter.Writer) {
|
||||
out.String(string(tt))
|
||||
}
|
||||
|
||||
// MarshalJSON satisfies json.Marshaler.
|
||||
func (tt TargetType) MarshalJSON() ([]byte, error) {
|
||||
return easyjson.Marshal(tt)
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON satisfies easyjson.Unmarshaler.
|
||||
func (tt *TargetType) UnmarshalEasyJSON(in *jlexer.Lexer) {
|
||||
z := TargetType(in.String())
|
||||
switch z {
|
||||
%s
|
||||
|
||||
default:
|
||||
*tt = z
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON satisfies json.Unmarshaler.
|
||||
func (tt *TargetType) UnmarshalJSON(buf []byte) error {
|
||||
return easyjson.Unmarshal(buf, tt)
|
||||
}
|
||||
`
|
||||
)
|
@ -1,79 +0,0 @@
|
||||
package client
|
||||
|
||||
// Code generated by gen.go. DO NOT EDIT.
|
||||
|
||||
import (
|
||||
easyjson "github.com/mailru/easyjson"
|
||||
jlexer "github.com/mailru/easyjson/jlexer"
|
||||
jwriter "github.com/mailru/easyjson/jwriter"
|
||||
)
|
||||
|
||||
// TargetType are the types of targets available in Chrome.
|
||||
type TargetType string
|
||||
|
||||
// TargetType values.
|
||||
const (
|
||||
App TargetType = "app"
|
||||
BackgroundPage TargetType = "background_page"
|
||||
Browser TargetType = "browser"
|
||||
External TargetType = "external"
|
||||
Iframe TargetType = "iframe"
|
||||
Other TargetType = "other"
|
||||
Page TargetType = "page"
|
||||
ServiceWorker TargetType = "service_worker"
|
||||
SharedWorker TargetType = "shared_worker"
|
||||
Webview TargetType = "webview"
|
||||
Worker TargetType = "worker"
|
||||
)
|
||||
|
||||
// String satisfies stringer.
|
||||
func (tt TargetType) String() string {
|
||||
return string(tt)
|
||||
}
|
||||
|
||||
// MarshalEasyJSON satisfies easyjson.Marshaler.
|
||||
func (tt TargetType) MarshalEasyJSON(out *jwriter.Writer) {
|
||||
out.String(string(tt))
|
||||
}
|
||||
|
||||
// MarshalJSON satisfies json.Marshaler.
|
||||
func (tt TargetType) MarshalJSON() ([]byte, error) {
|
||||
return easyjson.Marshal(tt)
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON satisfies easyjson.Unmarshaler.
|
||||
func (tt *TargetType) UnmarshalEasyJSON(in *jlexer.Lexer) {
|
||||
z := TargetType(in.String())
|
||||
switch z {
|
||||
case App:
|
||||
*tt = App
|
||||
case BackgroundPage:
|
||||
*tt = BackgroundPage
|
||||
case Browser:
|
||||
*tt = Browser
|
||||
case External:
|
||||
*tt = External
|
||||
case Iframe:
|
||||
*tt = Iframe
|
||||
case Other:
|
||||
*tt = Other
|
||||
case Page:
|
||||
*tt = Page
|
||||
case ServiceWorker:
|
||||
*tt = ServiceWorker
|
||||
case SharedWorker:
|
||||
*tt = SharedWorker
|
||||
case Webview:
|
||||
*tt = Webview
|
||||
case Worker:
|
||||
*tt = Worker
|
||||
|
||||
default:
|
||||
*tt = z
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON satisfies json.Unmarshaler.
|
||||
func (tt *TargetType) UnmarshalJSON(buf []byte) error {
|
||||
return easyjson.Unmarshal(buf, tt)
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultReadBufferSize is the default maximum read buffer size.
|
||||
DefaultReadBufferSize = 25 * 1024 * 1024
|
||||
|
||||
// DefaultWriteBufferSize is the default maximum write buffer size.
|
||||
DefaultWriteBufferSize = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
// Transport is the common interface to send/receive messages to a target.
|
||||
type Transport interface {
|
||||
Read() ([]byte, error)
|
||||
Write([]byte) error
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Conn wraps a gorilla/websocket.Conn connection.
|
||||
type Conn struct {
|
||||
*websocket.Conn
|
||||
}
|
||||
|
||||
// Read reads the next websocket message.
|
||||
func (c *Conn) Read() ([]byte, error) {
|
||||
_, buf, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// Write writes a websocket message.
|
||||
func (c *Conn) Write(buf []byte) error {
|
||||
return c.WriteMessage(websocket.TextMessage, buf)
|
||||
}
|
||||
|
||||
// Dial dials the specified target's websocket URL.
|
||||
//
|
||||
// Note: uses gorilla/websocket.
|
||||
func Dial(urlstr string, opts ...DialOption) (Transport, error) {
|
||||
d := &websocket.Dialer{
|
||||
ReadBufferSize: DefaultReadBufferSize,
|
||||
WriteBufferSize: DefaultWriteBufferSize,
|
||||
}
|
||||
|
||||
// apply opts
|
||||
for _, o := range opts {
|
||||
o(d)
|
||||
}
|
||||
|
||||
// connect
|
||||
conn, _, err := d.Dial(urlstr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Conn{conn}, nil
|
||||
}
|
||||
|
||||
// DialOption is a dial option.
|
||||
type DialOption func(*websocket.Dialer)
|
152
conn.go
Normal file
152
conn.go
Normal file
@ -0,0 +1,152 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/chromedp/cdproto"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/mailru/easyjson"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultReadBufferSize is the default maximum read buffer size.
|
||||
DefaultReadBufferSize = 25 * 1024 * 1024
|
||||
|
||||
// DefaultWriteBufferSize is the default maximum write buffer size.
|
||||
DefaultWriteBufferSize = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
// Transport is the common interface to send/receive messages to a target.
|
||||
type Transport interface {
|
||||
Read() (*cdproto.Message, error)
|
||||
Write(*cdproto.Message) error
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Conn wraps a gorilla/websocket.Conn connection.
|
||||
type Conn struct {
|
||||
*websocket.Conn
|
||||
dbgf func(string, ...interface{})
|
||||
}
|
||||
|
||||
// DialContext dials the specified websocket URL using gorilla/websocket.
|
||||
func DialContext(ctx context.Context, urlstr string, opts ...DialOption) (*Conn, error) {
|
||||
d := &websocket.Dialer{
|
||||
ReadBufferSize: DefaultReadBufferSize,
|
||||
WriteBufferSize: DefaultWriteBufferSize,
|
||||
}
|
||||
|
||||
// connect
|
||||
conn, _, err := d.DialContext(ctx, urlstr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// apply opts
|
||||
c := &Conn{
|
||||
Conn: conn,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(c)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Read reads the next message.
|
||||
func (c *Conn) Read() (*cdproto.Message, error) {
|
||||
// get websocket reader
|
||||
typ, r, err := c.NextReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if typ != websocket.TextMessage {
|
||||
return nil, ErrInvalidWebsocketMessage
|
||||
}
|
||||
|
||||
// when dbgf defined, buffer, log, unmarshal
|
||||
if c.dbgf != nil {
|
||||
// buffer output
|
||||
buf, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.dbgf("<- %s", string(buf))
|
||||
msg := new(cdproto.Message)
|
||||
if err = easyjson.Unmarshal(buf, msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// unmarshal direct from reader
|
||||
msg := new(cdproto.Message)
|
||||
if err = easyjson.UnmarshalFromReader(r, msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// Write writes a message.
|
||||
func (c *Conn) Write(msg *cdproto.Message) error {
|
||||
w, err := c.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.dbgf != nil {
|
||||
var buf []byte
|
||||
buf, err = easyjson.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.dbgf("-> %s", string(buf))
|
||||
_, err = w.Write(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// direct marshal
|
||||
_, err = easyjson.MarshalToWriter(msg, w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
// ForceIP forces the host component in urlstr to be an IP address.
|
||||
//
|
||||
// Since Chrome 66+, Chrome DevTools Protocol clients connecting to a browser
|
||||
// must send the "Host:" header as either an IP address, or "localhost".
|
||||
func ForceIP(urlstr string) string {
|
||||
if i := strings.Index(urlstr, "://"); i != -1 {
|
||||
scheme := urlstr[:i+3]
|
||||
host, port, path := urlstr[len(scheme)+3:], "", ""
|
||||
if i := strings.Index(host, "/"); i != -1 {
|
||||
host, path = host[:i], host[i:]
|
||||
}
|
||||
if i := strings.Index(host, ":"); i != -1 {
|
||||
host, port = host[:i], host[i:]
|
||||
}
|
||||
if addr, err := net.ResolveIPAddr("ip", host); err == nil {
|
||||
urlstr = scheme + addr.IP.String() + port + path
|
||||
}
|
||||
}
|
||||
return urlstr
|
||||
}
|
||||
|
||||
// DialOption is a dial option.
|
||||
type DialOption func(*Conn)
|
||||
|
||||
// WithConnDebugf is a dial option to set a protocol logger.
|
||||
func WithConnDebugf(f func(string, ...interface{})) DialOption {
|
||||
return func(c *Conn) {
|
||||
c.dbgf = f
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
SRC=$(realpath $(cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/../)
|
||||
|
||||
pushd $SRC &> /dev/null
|
||||
|
||||
gometalinter \
|
||||
--disable=aligncheck \
|
||||
--enable=misspell \
|
||||
--enable=gofmt \
|
||||
--deadline=100s \
|
||||
--cyclo-over=25 \
|
||||
--sort=path \
|
||||
--exclude='\(defer (.+?)\)\) \(errcheck\)$' \
|
||||
--exclude='/easyjson\.go.*(passes|copies) lock' \
|
||||
--exclude='/easyjson\.go.*ineffectual assignment' \
|
||||
--exclude='/easyjson\.go.*unnecessary conversion' \
|
||||
--exclude='/easyjson\.go.*this value of key is never used' \
|
||||
--exclude='/easyjson\.go.*\((gocyclo|golint|goconst|staticcheck)\)$' \
|
||||
--exclude='^cdp/.*Potential hardcoded credentials' \
|
||||
--exclude='^cdp/cdp\.go.*UnmarshalEasyJSON.*\(gocyclo\)$' \
|
||||
--exclude='^cdp/cdputil/cdputil\.go.*UnmarshalMessage.*\(gocyclo\)$' \
|
||||
--exclude='^cmd/chromedp-gen/.*\((gocyclo|interfacer)\)$' \
|
||||
--exclude='^cmd/chromedp-proxy/main\.go.*\(gas\)$' \
|
||||
--exclude='^cmd/chromedp-gen/fixup/fixup\.go.*\(goconst\)$' \
|
||||
--exclude='^cmd/chromedp-gen/internal/enum\.go.*unreachable' \
|
||||
--exclude='^cmd/chromedp-gen/(main|domain-gen)\.go.*\(gas\)$' \
|
||||
--exclude='^examples/[a-z]+/main\.go.*\(errcheck\)$' \
|
||||
--exclude='^kb/gen\.go.*\((gas|vet)\)$' \
|
||||
--exclude='^runner/.*\(gas\)$' \
|
||||
--exclude='^handler\.go.*cmd can be easyjson\.Marshaler' \
|
||||
./...
|
||||
|
||||
popd &> /dev/null
|
@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
TMP=$(mktemp -d /tmp/google-chrome.XXXXX)
|
||||
|
||||
google-chrome \
|
||||
--user-data-dir=$TMP \
|
||||
--remote-debugging-port=9222 \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
about:blank
|
@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASE=$(realpath $(cd -P $GOPATH/src/github.com/chromedp && pwd))
|
||||
|
||||
FILES=$(find $BASE/{chromedp*,goquery,examples} -type f -iname \*.go -not -iname \*.qtpl.go -print0|wc -l --files0-from=-|head -n -1)$'\n'
|
||||
|
||||
AUTOG=$(find $BASE/cdproto/ -type f -iname \*.go -not -iname \*easyjson\* -print0|wc -l --files0-from=-|head -n -1)
|
||||
|
||||
if [ "$1" != "--total" ]; then
|
||||
echo -e "code:\n$FILES\n\ngenerated:\n$AUTOG"
|
||||
else
|
||||
echo "code: $(awk '{s+=$1} END {print s}' <<< "$FILES")"
|
||||
echo "generated: $(awk '{s+=$1} END {print s}' <<< "$AUTOG")"
|
||||
fi
|
@ -10,6 +10,9 @@ func (err Error) Error() string {
|
||||
|
||||
// Error types.
|
||||
const (
|
||||
// ErrInvalidWebsocketMessage is the invalid websocket message.
|
||||
ErrInvalidWebsocketMessage Error = "invalid websocket message"
|
||||
|
||||
// ErrInvalidDimensions is the invalid dimensions error.
|
||||
ErrInvalidDimensions Error = "invalid dimensions"
|
||||
|
||||
@ -39,4 +42,7 @@ const (
|
||||
|
||||
// ErrInvalidHandler is the invalid handler error.
|
||||
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
|
||||
// the result of the script evaluation to res.
|
||||
//
|
||||
// When res is a type other than *[]byte, or **chromedp/cdp/runtime.RemoteObject,
|
||||
// When res is a type other than *[]byte, or **chromedp/cdproto/runtime.RemoteObject,
|
||||
// then the result of the script evaluation will be returned "by value" (ie,
|
||||
// JSON-encoded), and subsequently an attempt will be made to json.Unmarshal
|
||||
// the script result to res.
|
||||
@ -27,7 +27,7 @@ func Evaluate(expression string, res interface{}, opts ...EvaluateOption) Action
|
||||
panic("res cannot be nil")
|
||||
}
|
||||
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
// set up parameters
|
||||
p := runtime.Evaluate(expression)
|
||||
switch res.(type) {
|
||||
@ -42,7 +42,7 @@ func Evaluate(expression string, res interface{}, opts ...EvaluateOption) Action
|
||||
}
|
||||
|
||||
// evaluate
|
||||
v, exp, err := p.Do(ctxt, h)
|
||||
v, exp, err := p.Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
100
example_test.go
Normal file
100
example_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
package chromedp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.loafle.net/commons_go/chromedp"
|
||||
)
|
||||
|
||||
func ExampleTitle() {
|
||||
ctx, cancel := chromedp.NewContext(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var title string
|
||||
if err := chromedp.Run(ctx,
|
||||
chromedp.Navigate("https://git.loafle.net/commons_go/chromedp/issues"),
|
||||
chromedp.WaitVisible("#start-of-content", chromedp.ByID),
|
||||
chromedp.Title(&title),
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(title)
|
||||
|
||||
// no expected output, to not run this test as part of 'go test'; it's
|
||||
// too slow, requiring internet access.
|
||||
}
|
||||
|
||||
func ExampleExecAllocator() {
|
||||
dir, err := ioutil.TempDir("", "chromedp-example")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
opts := []chromedp.ExecAllocatorOption{
|
||||
chromedp.NoFirstRun,
|
||||
chromedp.NoDefaultBrowserCheck,
|
||||
chromedp.Headless,
|
||||
chromedp.DisableGPU,
|
||||
chromedp.UserDataDir(dir),
|
||||
}
|
||||
|
||||
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
defer cancel()
|
||||
|
||||
// also set up a custom logger
|
||||
taskCtx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
|
||||
defer cancel()
|
||||
|
||||
// ensure that the browser process is started
|
||||
if err := chromedp.Run(taskCtx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, "DevToolsActivePort")
|
||||
bs, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
lines := bytes.Split(bs, []byte("\n"))
|
||||
fmt.Printf("DevToolsActivePort has %d lines\n", len(lines))
|
||||
|
||||
// Output:
|
||||
// DevToolsActivePort has 2 lines
|
||||
}
|
||||
|
||||
func ExampleNewContext_manyTabs() {
|
||||
// new browser, first tab
|
||||
ctx1, cancel := chromedp.NewContext(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// ensure the first tab is created
|
||||
if err := chromedp.Run(ctx1); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// same browser, second tab
|
||||
ctx2, _ := chromedp.NewContext(ctx1)
|
||||
|
||||
// ensure the second tab is created
|
||||
if err := chromedp.Run(ctx2); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c1 := chromedp.FromContext(ctx1)
|
||||
c2 := chromedp.FromContext(ctx2)
|
||||
|
||||
fmt.Printf("Same browser: %t\n", c1.Browser == c2.Browser)
|
||||
fmt.Printf("Same tab: %t\n", c1.Target == c2.Target)
|
||||
|
||||
// Output:
|
||||
// Same browser: true
|
||||
// Same tab: false
|
||||
}
|
9
go.mod
9
go.mod
@ -1,9 +1,10 @@
|
||||
module github.com/chromedp/chromedp
|
||||
module git.loafle.net/commons_go/chromedp
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2
|
||||
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a
|
||||
github.com/disintegration/imaging v1.6.0
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9 // indirect
|
||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983
|
||||
)
|
||||
|
11
go.sum
11
go.sum
@ -1,5 +1,5 @@
|
||||
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2 h1:4Ck8YOuS0G3+0xMb80cDSff7QpUolhSc0PGyfagbcdA=
|
||||
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
||||
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a h1:GZPhzysmNSpFnYVSzixFV/ECNILkkn5HJon7AOUNizg=
|
||||
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
||||
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
|
||||
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
@ -7,10 +7,7 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA
|
||||
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls=
|
||||
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/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-20190221075403-6243d8e04c3f h1:B6PQkurxGG1rqEX96oE14gbj8bqvYC5dtks9r5uGmlE=
|
||||
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 h1:wL11wNW7dhKIcRCHSm4sHKPWz0tt4mwBsVodG7+Xyqg=
|
||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9 h1:+vH8qNweCrORN49012OX3h0oWEXO3p+rRnpAGQinddk=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
657
handler.go
657
handler.go
@ -1,657 +0,0 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
goruntime "runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
|
||||
"github.com/chromedp/cdproto"
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/css"
|
||||
"github.com/chromedp/cdproto/dom"
|
||||
"github.com/chromedp/cdproto/inspector"
|
||||
"github.com/chromedp/cdproto/log"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
|
||||
"github.com/chromedp/chromedp/client"
|
||||
)
|
||||
|
||||
// TargetHandler manages a Chrome DevTools Protocol target.
|
||||
type TargetHandler struct {
|
||||
conn client.Transport
|
||||
|
||||
// frames is the set of encountered frames.
|
||||
frames map[cdp.FrameID]*cdp.Frame
|
||||
|
||||
// cur is the current top level frame.
|
||||
cur *cdp.Frame
|
||||
|
||||
// qcmd is the outgoing message queue.
|
||||
qcmd chan *cdproto.Message
|
||||
|
||||
// qres is the incoming command result queue.
|
||||
qres chan *cdproto.Message
|
||||
|
||||
// qevents is the incoming event queue.
|
||||
qevents chan *cdproto.Message
|
||||
|
||||
// detached is closed when the detached event is received.
|
||||
detached chan *inspector.EventDetached
|
||||
|
||||
pageWaitGroup, domWaitGroup *sync.WaitGroup
|
||||
|
||||
// last is the last sent message identifier.
|
||||
last int64
|
||||
lastm sync.Mutex
|
||||
|
||||
// res is the id->result channel map.
|
||||
res map[int64]chan *cdproto.Message
|
||||
resrw sync.RWMutex
|
||||
|
||||
// logging funcs
|
||||
logf, debugf, errf func(string, ...interface{})
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// NewTargetHandler creates a new handler for the specified client target.
|
||||
func NewTargetHandler(t client.Target, logf, debugf, errf func(string, ...interface{})) (*TargetHandler, error) {
|
||||
conn, err := client.Dial(t.GetWebsocketURL())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TargetHandler{
|
||||
conn: conn,
|
||||
logf: logf,
|
||||
debugf: debugf,
|
||||
errf: errf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run starts the processing of commands and events of the client target
|
||||
// provided to NewTargetHandler.
|
||||
//
|
||||
// Callers can stop Run by closing the passed context.
|
||||
func (h *TargetHandler) Run(ctxt context.Context) error {
|
||||
// reset
|
||||
h.Lock()
|
||||
h.frames = make(map[cdp.FrameID]*cdp.Frame)
|
||||
h.qcmd = make(chan *cdproto.Message)
|
||||
h.qres = make(chan *cdproto.Message)
|
||||
h.qevents = make(chan *cdproto.Message)
|
||||
h.res = make(map[int64]chan *cdproto.Message)
|
||||
h.detached = make(chan *inspector.EventDetached, 1)
|
||||
h.pageWaitGroup = new(sync.WaitGroup)
|
||||
h.domWaitGroup = new(sync.WaitGroup)
|
||||
h.Unlock()
|
||||
|
||||
// run
|
||||
go h.run(ctxt)
|
||||
|
||||
// enable domains
|
||||
for _, a := range []Action{
|
||||
log.Enable(),
|
||||
runtime.Enable(),
|
||||
//network.Enable(),
|
||||
inspector.Enable(),
|
||||
page.Enable(),
|
||||
dom.Enable(),
|
||||
css.Enable(),
|
||||
} {
|
||||
if err := a.Do(ctxt, h); err != nil {
|
||||
return fmt.Errorf("unable to execute %s: %v", reflect.TypeOf(a), err)
|
||||
}
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
|
||||
// get page resources
|
||||
tree, err := page.GetResourceTree().Do(ctxt, h)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get resource tree: %v", err)
|
||||
}
|
||||
|
||||
h.frames[tree.Frame.ID] = tree.Frame
|
||||
h.cur = tree.Frame
|
||||
|
||||
for _, c := range tree.ChildFrames {
|
||||
h.frames[c.Frame.ID] = c.Frame
|
||||
}
|
||||
|
||||
h.Unlock()
|
||||
|
||||
h.documentUpdated(ctxt)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// run handles the actual message processing to / from the web socket connection.
|
||||
func (h *TargetHandler) run(ctxt context.Context) {
|
||||
defer h.conn.Close()
|
||||
|
||||
// add cancel to context
|
||||
ctxt, cancel := context.WithCancel(ctxt)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
default:
|
||||
msg, err := h.read()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case msg.Method != "":
|
||||
h.qevents <- msg
|
||||
|
||||
case msg.ID != 0:
|
||||
h.qres <- msg
|
||||
|
||||
default:
|
||||
h.errf("ignoring malformed incoming message (missing id or method): %#v", msg)
|
||||
}
|
||||
|
||||
case <-h.detached:
|
||||
// FIXME: should log when detached, and reason
|
||||
return
|
||||
|
||||
case <-ctxt.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// process queues
|
||||
for {
|
||||
select {
|
||||
case ev := <-h.qevents:
|
||||
err := h.processEvent(ctxt, ev)
|
||||
if err != nil {
|
||||
h.errf("could not process event %s: %v", ev.Method, err)
|
||||
}
|
||||
|
||||
case res := <-h.qres:
|
||||
err := h.processResult(res)
|
||||
if err != nil {
|
||||
h.errf("could not process result for message %d: %v", res.ID, err)
|
||||
}
|
||||
|
||||
case cmd := <-h.qcmd:
|
||||
err := h.processCommand(cmd)
|
||||
if err != nil {
|
||||
h.errf("could not process command message %d: %v", cmd.ID, err)
|
||||
}
|
||||
|
||||
case <-ctxt.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read reads a message from the client connection.
|
||||
func (h *TargetHandler) read() (*cdproto.Message, error) {
|
||||
// read
|
||||
buf, err := h.conn.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.debugf("-> %s", string(buf))
|
||||
|
||||
// unmarshal
|
||||
msg := new(cdproto.Message)
|
||||
err = json.Unmarshal(buf, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// processEvent processes an incoming event.
|
||||
func (h *TargetHandler) processEvent(ctxt context.Context, msg *cdproto.Message) error {
|
||||
if msg == nil {
|
||||
return ErrChannelClosed
|
||||
}
|
||||
|
||||
// unmarshal
|
||||
ev, err := cdproto.UnmarshalMessage(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch e := ev.(type) {
|
||||
case *inspector.EventDetached:
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
h.detached <- e
|
||||
return nil
|
||||
|
||||
case *dom.EventDocumentUpdated:
|
||||
h.domWaitGroup.Wait()
|
||||
go h.documentUpdated(ctxt)
|
||||
return nil
|
||||
}
|
||||
|
||||
d := msg.Method.Domain()
|
||||
if d != "Page" && d != "DOM" {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch d {
|
||||
case "Page":
|
||||
h.pageWaitGroup.Add(1)
|
||||
go h.pageEvent(ctxt, ev)
|
||||
|
||||
case "DOM":
|
||||
h.domWaitGroup.Add(1)
|
||||
go h.domEvent(ctxt, ev)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// documentUpdated handles the document updated event, retrieving the document
|
||||
// root for the root frame.
|
||||
func (h *TargetHandler) documentUpdated(ctxt context.Context) {
|
||||
f, err := h.WaitFrame(ctxt, cdp.EmptyFrameID)
|
||||
if err != nil {
|
||||
h.errf("could not get current frame: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
|
||||
// invalidate nodes
|
||||
if f.Root != nil {
|
||||
close(f.Root.Invalidated)
|
||||
}
|
||||
|
||||
f.Nodes = make(map[cdp.NodeID]*cdp.Node)
|
||||
f.Root, err = dom.GetDocument().WithPierce(true).Do(ctxt, h)
|
||||
if err != nil {
|
||||
h.errf("could not retrieve document root for %s: %v", f.ID, err)
|
||||
return
|
||||
}
|
||||
f.Root.Invalidated = make(chan struct{})
|
||||
walk(f.Nodes, f.Root)
|
||||
}
|
||||
|
||||
// processResult processes an incoming command result.
|
||||
func (h *TargetHandler) processResult(msg *cdproto.Message) error {
|
||||
h.resrw.RLock()
|
||||
defer h.resrw.RUnlock()
|
||||
|
||||
ch, ok := h.res[msg.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("id %d not present in res map", msg.ID)
|
||||
}
|
||||
defer close(ch)
|
||||
|
||||
ch <- msg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processCommand writes a command to the client connection.
|
||||
func (h *TargetHandler) processCommand(cmd *cdproto.Message) error {
|
||||
// marshal
|
||||
buf, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.debugf("<- %s", string(buf))
|
||||
|
||||
return h.conn.Write(buf)
|
||||
}
|
||||
|
||||
// emptyObj is an empty JSON object message.
|
||||
var emptyObj = easyjson.RawMessage([]byte(`{}`))
|
||||
|
||||
// Execute executes commandType against the endpoint passed to Run, using the
|
||||
// provided context and params, decoding the result of the command to res.
|
||||
func (h *TargetHandler) Execute(ctxt context.Context, methodType string, params json.Marshaler, res json.Unmarshaler) error {
|
||||
var paramsBuf easyjson.RawMessage
|
||||
if params == nil {
|
||||
paramsBuf = emptyObj
|
||||
} else {
|
||||
var err error
|
||||
paramsBuf, err = json.Marshal(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
id := h.next()
|
||||
|
||||
// save channel
|
||||
ch := make(chan *cdproto.Message, 1)
|
||||
h.resrw.Lock()
|
||||
h.res[id] = ch
|
||||
h.resrw.Unlock()
|
||||
|
||||
// queue message
|
||||
h.qcmd <- &cdproto.Message{
|
||||
ID: id,
|
||||
Method: cdproto.MethodType(methodType),
|
||||
Params: paramsBuf,
|
||||
}
|
||||
|
||||
errch := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(errch)
|
||||
|
||||
select {
|
||||
case msg := <-ch:
|
||||
switch {
|
||||
case msg == nil:
|
||||
errch <- ErrChannelClosed
|
||||
|
||||
case msg.Error != nil:
|
||||
errch <- msg.Error
|
||||
|
||||
case res != nil:
|
||||
errch <- json.Unmarshal(msg.Result, res)
|
||||
}
|
||||
|
||||
case <-ctxt.Done():
|
||||
errch <- ctxt.Err()
|
||||
}
|
||||
|
||||
h.resrw.Lock()
|
||||
defer h.resrw.Unlock()
|
||||
|
||||
delete(h.res, id)
|
||||
}()
|
||||
|
||||
return <-errch
|
||||
}
|
||||
|
||||
// next returns the next message id.
|
||||
func (h *TargetHandler) next() int64 {
|
||||
h.lastm.Lock()
|
||||
defer h.lastm.Unlock()
|
||||
h.last++
|
||||
return h.last
|
||||
}
|
||||
|
||||
// GetRoot returns the current top level frame's root document node.
|
||||
func (h *TargetHandler) GetRoot(ctxt context.Context) (*cdp.Node, error) {
|
||||
var root *cdp.Node
|
||||
|
||||
for {
|
||||
var cur *cdp.Frame
|
||||
select {
|
||||
default:
|
||||
h.RLock()
|
||||
cur = h.cur
|
||||
if cur != nil {
|
||||
cur.RLock()
|
||||
root = cur.Root
|
||||
cur.RUnlock()
|
||||
}
|
||||
h.RUnlock()
|
||||
|
||||
if cur != nil && root != nil {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
time.Sleep(DefaultCheckDuration)
|
||||
|
||||
case <-ctxt.Done():
|
||||
return nil, ctxt.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetActive sets the currently active frame after a successful navigation.
|
||||
func (h *TargetHandler) SetActive(ctxt context.Context, id cdp.FrameID) error {
|
||||
var err error
|
||||
|
||||
// get frame
|
||||
f, err := h.WaitFrame(ctxt, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
h.cur = f
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitFrame waits for a frame to be loaded using the provided context.
|
||||
func (h *TargetHandler) WaitFrame(ctxt context.Context, id cdp.FrameID) (*cdp.Frame, error) {
|
||||
// TODO: fix this
|
||||
timeout := time.After(10 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
default:
|
||||
var f *cdp.Frame
|
||||
var ok bool
|
||||
|
||||
h.RLock()
|
||||
if id == cdp.EmptyFrameID {
|
||||
f, ok = h.cur, h.cur != nil
|
||||
} else {
|
||||
f, ok = h.frames[id]
|
||||
}
|
||||
h.RUnlock()
|
||||
|
||||
if ok {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
time.Sleep(DefaultCheckDuration)
|
||||
|
||||
case <-ctxt.Done():
|
||||
return nil, ctxt.Err()
|
||||
|
||||
case <-timeout:
|
||||
return nil, fmt.Errorf("timeout waiting for frame `%s`", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WaitNode waits for a node to be loaded using the provided context.
|
||||
func (h *TargetHandler) WaitNode(ctxt context.Context, f *cdp.Frame, id cdp.NodeID) (*cdp.Node, error) {
|
||||
// TODO: fix this
|
||||
timeout := time.After(10 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
default:
|
||||
var n *cdp.Node
|
||||
var ok bool
|
||||
|
||||
f.RLock()
|
||||
n, ok = f.Nodes[id]
|
||||
f.RUnlock()
|
||||
|
||||
if n != nil && ok {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
time.Sleep(DefaultCheckDuration)
|
||||
|
||||
case <-ctxt.Done():
|
||||
return nil, ctxt.Err()
|
||||
|
||||
case <-timeout:
|
||||
return nil, fmt.Errorf("timeout waiting for node `%d`", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pageEvent handles incoming page events.
|
||||
func (h *TargetHandler) pageEvent(ctxt context.Context, ev interface{}) {
|
||||
defer h.pageWaitGroup.Done()
|
||||
|
||||
var id cdp.FrameID
|
||||
var op frameOp
|
||||
|
||||
switch e := ev.(type) {
|
||||
case *page.EventFrameNavigated:
|
||||
h.Lock()
|
||||
h.frames[e.Frame.ID] = e.Frame
|
||||
if h.cur != nil && h.cur.ID == e.Frame.ID {
|
||||
h.cur = e.Frame
|
||||
}
|
||||
h.Unlock()
|
||||
return
|
||||
|
||||
case *page.EventFrameAttached:
|
||||
id, op = e.FrameID, frameAttached(e.ParentFrameID)
|
||||
|
||||
case *page.EventFrameDetached:
|
||||
id, op = e.FrameID, frameDetached
|
||||
|
||||
case *page.EventFrameStartedLoading:
|
||||
id, op = e.FrameID, frameStartedLoading
|
||||
|
||||
case *page.EventFrameStoppedLoading:
|
||||
id, op = e.FrameID, frameStoppedLoading
|
||||
|
||||
case *page.EventFrameScheduledNavigation:
|
||||
id, op = e.FrameID, frameScheduledNavigation
|
||||
|
||||
case *page.EventFrameClearedScheduledNavigation:
|
||||
id, op = e.FrameID, frameClearedScheduledNavigation
|
||||
|
||||
// ignored events
|
||||
case *page.EventDomContentEventFired:
|
||||
return
|
||||
case *page.EventLoadEventFired:
|
||||
return
|
||||
case *page.EventFrameResized:
|
||||
return
|
||||
case *page.EventLifecycleEvent:
|
||||
return
|
||||
|
||||
default:
|
||||
h.errf("unhandled page event %s", reflect.TypeOf(ev))
|
||||
return
|
||||
}
|
||||
|
||||
f, err := h.WaitFrame(ctxt, id)
|
||||
if err != nil {
|
||||
h.errf("could not get frame %s: %v", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
|
||||
op(f)
|
||||
}
|
||||
|
||||
// domEvent handles incoming DOM events.
|
||||
func (h *TargetHandler) domEvent(ctxt context.Context, ev interface{}) {
|
||||
defer h.domWaitGroup.Done()
|
||||
|
||||
// wait current frame
|
||||
f, err := h.WaitFrame(ctxt, cdp.EmptyFrameID)
|
||||
if err != nil {
|
||||
h.errf("could not process DOM event %s: %v", reflect.TypeOf(ev), err)
|
||||
return
|
||||
}
|
||||
|
||||
var id cdp.NodeID
|
||||
var op nodeOp
|
||||
|
||||
switch e := ev.(type) {
|
||||
case *dom.EventSetChildNodes:
|
||||
id, op = e.ParentID, setChildNodes(f.Nodes, e.Nodes)
|
||||
|
||||
case *dom.EventAttributeModified:
|
||||
id, op = e.NodeID, attributeModified(e.Name, e.Value)
|
||||
|
||||
case *dom.EventAttributeRemoved:
|
||||
id, op = e.NodeID, attributeRemoved(e.Name)
|
||||
|
||||
case *dom.EventInlineStyleInvalidated:
|
||||
if len(e.NodeIds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
id, op = e.NodeIds[0], inlineStyleInvalidated(e.NodeIds[1:])
|
||||
|
||||
case *dom.EventCharacterDataModified:
|
||||
id, op = e.NodeID, characterDataModified(e.CharacterData)
|
||||
|
||||
case *dom.EventChildNodeCountUpdated:
|
||||
id, op = e.NodeID, childNodeCountUpdated(e.ChildNodeCount)
|
||||
|
||||
case *dom.EventChildNodeInserted:
|
||||
if e.PreviousNodeID != cdp.EmptyNodeID {
|
||||
_, err = h.WaitNode(ctxt, f, e.PreviousNodeID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
id, op = e.ParentNodeID, childNodeInserted(f.Nodes, e.PreviousNodeID, e.Node)
|
||||
|
||||
case *dom.EventChildNodeRemoved:
|
||||
id, op = e.ParentNodeID, childNodeRemoved(f.Nodes, e.NodeID)
|
||||
|
||||
case *dom.EventShadowRootPushed:
|
||||
id, op = e.HostID, shadowRootPushed(f.Nodes, e.Root)
|
||||
|
||||
case *dom.EventShadowRootPopped:
|
||||
id, op = e.HostID, shadowRootPopped(f.Nodes, e.RootID)
|
||||
|
||||
case *dom.EventPseudoElementAdded:
|
||||
id, op = e.ParentID, pseudoElementAdded(f.Nodes, e.PseudoElement)
|
||||
|
||||
case *dom.EventPseudoElementRemoved:
|
||||
id, op = e.ParentID, pseudoElementRemoved(f.Nodes, e.PseudoElementID)
|
||||
|
||||
case *dom.EventDistributedNodesUpdated:
|
||||
id, op = e.InsertionPointID, distributedNodesUpdated(e.DistributedNodes)
|
||||
|
||||
default:
|
||||
h.errf("unhandled node event %s", reflect.TypeOf(ev))
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve node
|
||||
n, err := h.WaitNode(ctxt, f, id)
|
||||
if err != nil {
|
||||
s := strings.TrimSuffix(goruntime.FuncForPC(reflect.ValueOf(op).Pointer()).Name(), ".func1")
|
||||
i := strings.LastIndex(s, ".")
|
||||
if i != -1 {
|
||||
s = s[i+1:]
|
||||
}
|
||||
h.errf("could not perform (%s) operation on node %d (wait node): %v", s, id, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
|
||||
op(n)
|
||||
}
|
36
input.go
36
input.go
@ -3,13 +3,12 @@ package chromedp
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/dom"
|
||||
"github.com/chromedp/cdproto/input"
|
||||
|
||||
"github.com/chromedp/chromedp/kb"
|
||||
"git.loafle.net/commons_go/chromedp/kb"
|
||||
)
|
||||
|
||||
// MouseAction is a mouse action.
|
||||
@ -27,7 +26,7 @@ func MouseAction(typ input.MouseType, x, y int64, opts ...MouseOption) Action {
|
||||
// MouseClickXY sends a left mouse button click (ie, mousePressed and
|
||||
// mouseReleased event) at the X, Y location.
|
||||
func MouseClickXY(x, y int64, opts ...MouseOption) Action {
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
me := &input.DispatchMouseEventParams{
|
||||
Type: input.MousePressed,
|
||||
X: float64(x),
|
||||
@ -41,13 +40,12 @@ func MouseClickXY(x, y int64, opts ...MouseOption) Action {
|
||||
me = o(me)
|
||||
}
|
||||
|
||||
err := me.Do(ctxt, h)
|
||||
if err != nil {
|
||||
if err := me.Do(ctx, h); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
me.Type = input.MouseReleased
|
||||
return me.Do(ctxt, h)
|
||||
return me.Do(ctx, h)
|
||||
})
|
||||
}
|
||||
|
||||
@ -57,16 +55,14 @@ func MouseClickXY(x, y int64, opts ...MouseOption) Action {
|
||||
// Note that the window will be scrolled if the node is not within the window's
|
||||
// viewport.
|
||||
func MouseClickNode(n *cdp.Node, opts ...MouseOption) Action {
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
var err error
|
||||
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
var pos []int
|
||||
err = EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, n.FullXPath()), &pos).Do(ctxt, h)
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, n.FullXPath()), &pos).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
box, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
||||
box, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -84,7 +80,7 @@ func MouseClickNode(n *cdp.Node, opts ...MouseOption) Action {
|
||||
x /= int64(c / 2)
|
||||
y /= int64(c / 2)
|
||||
|
||||
return MouseClickXY(x, y, opts...).Do(ctxt, h)
|
||||
return MouseClickXY(x, y, opts...).Do(ctx, h)
|
||||
})
|
||||
}
|
||||
|
||||
@ -153,19 +149,13 @@ func ClickCount(n int) MouseOption {
|
||||
// Please see the chromedp/kb package for implementation details and the list
|
||||
// of well-known keys.
|
||||
func KeyAction(keys string, opts ...KeyOption) Action {
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
var err error
|
||||
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
for _, r := range keys {
|
||||
for _, k := range kb.Encode(r) {
|
||||
err = k.Do(ctxt, h)
|
||||
if err != nil {
|
||||
if err := k.Do(ctx, h); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move to context
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -174,13 +164,13 @@ func KeyAction(keys string, opts ...KeyOption) Action {
|
||||
|
||||
// KeyActionNode dispatches a key event on a node.
|
||||
func KeyActionNode(n *cdp.Node, keys string, opts ...KeyOption) Action {
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
err := dom.Focus().WithNodeID(n.NodeID).Do(ctxt, h)
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
err := dom.Focus().WithNodeID(n.NodeID).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return KeyAction(keys, opts...).Do(ctxt, h)
|
||||
return KeyAction(keys, opts...).Do(ctx, h)
|
||||
})
|
||||
}
|
||||
|
||||
|
146
input_test.go
146
input_test.go
@ -4,35 +4,28 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/input"
|
||||
)
|
||||
|
||||
const (
|
||||
// inViewportJS is a javascript snippet that will get the specified node
|
||||
// position relative to the viewport and returns true if the specified node
|
||||
// is within the window's viewport.
|
||||
inViewportJS = `(function(a) {
|
||||
// inViewportJS is a javascript snippet that will get the specified node
|
||||
// position relative to the viewport and returns true if the specified node
|
||||
// is within the window's viewport.
|
||||
const inViewportJS = `(function(a) {
|
||||
var r = a[0].getBoundingClientRect();
|
||||
return r.top >= 0 && r.left >= 0 && r.bottom <= window.innerHeight && r.right <= window.innerWidth;
|
||||
})($x('%s'))`
|
||||
)
|
||||
|
||||
func TestMouseClickXY(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
ctx, cancel := testAllocate(t, "input.html")
|
||||
defer cancel()
|
||||
|
||||
c := testAllocate(t, "input.html")
|
||||
defer c.Release()
|
||||
|
||||
err = c.Run(defaultContext, Sleep(100*time.Millisecond))
|
||||
if err != nil {
|
||||
if err := Run(ctx, WaitVisible(`#input1`, ByID)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
x, y int64
|
||||
}{
|
||||
@ -43,18 +36,14 @@ func TestMouseClickXY(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
err = c.Run(defaultContext, MouseClickXY(test.x, test.y))
|
||||
if err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
var xstr, ystr string
|
||||
err = c.Run(defaultContext, Value("#input1", &xstr, ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
MouseClickXY(test.x, test.y),
|
||||
Value("#input1", &xstr, ByID),
|
||||
); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
x, err := strconv.ParseInt(xstr, 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
@ -62,11 +51,10 @@ func TestMouseClickXY(t *testing.T) {
|
||||
if x != test.x {
|
||||
t.Fatalf("test %d expected x to be: %d, got: %d", i, test.x, x)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, Value("#input2", &ystr, ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Value("#input2", &ystr, ByID)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
y, err := strconv.ParseInt(ystr, 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
@ -88,40 +76,34 @@ func TestMouseClickNode(t *testing.T) {
|
||||
{"button2", "foo", ButtonType(input.ButtonNone), ByID},
|
||||
{"button2", "bar", ButtonType(input.ButtonLeft), ByID},
|
||||
{"button2", "bar-middle", ButtonType(input.ButtonMiddle), ByID},
|
||||
{"input3", "foo", ButtonModifiers(input.ModifierNone), ByID},
|
||||
{"input3", "bar-right", ButtonType(input.ButtonRight), ByID},
|
||||
{"input3", "bar-right", ButtonModifiers(input.ModifierNone), ByID},
|
||||
{"input3", "bar-right", Button("right"), ByID},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "input.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "input.html")
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
var nodes []*cdp.Node
|
||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, MouseClickNode(nodes[0], test.opt))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
var value string
|
||||
err = c.Run(defaultContext, Value("#input3", &value, ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
MouseClickNode(nodes[0], test.opt),
|
||||
Value("#input3", &value, ByID),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if value != test.exp {
|
||||
t.Fatalf("expected to have value %s, got: %s", test.exp, value)
|
||||
}
|
||||
@ -143,45 +125,42 @@ func TestMouseClickOffscreenNode(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "input.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "input.html")
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
var nodes []*cdp.Node
|
||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
||||
}
|
||||
|
||||
var ok bool
|
||||
err = c.Run(defaultContext, EvaluateAsDevTools(fmt.Sprintf(inViewportJS, nodes[0].FullXPath()), &ok))
|
||||
if err != nil {
|
||||
if err := Run(ctx, EvaluateAsDevTools(fmt.Sprintf(inViewportJS, nodes[0].FullXPath()), &ok)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
t.Fatal("expected node to be offscreen")
|
||||
}
|
||||
|
||||
for i := test.exp; i > 0; i-- {
|
||||
err = c.Run(defaultContext, MouseClickNode(nodes[0]))
|
||||
if err != nil {
|
||||
if err := Run(ctx, MouseClickNode(nodes[0])); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
var value int
|
||||
err = c.Run(defaultContext, Evaluate("window.document.test_i", &value))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Evaluate("window.document.test_i", &value)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if value != test.exp {
|
||||
t.Fatalf("expected to have value %d, got: %d", test.exp, value)
|
||||
}
|
||||
@ -205,37 +184,33 @@ func TestKeyAction(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "input.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "input.html")
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
var nodes []*cdp.Node
|
||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, Focus(test.sel, test.by))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, KeyAction(test.exp))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
Focus(test.sel, test.by),
|
||||
KeyAction(test.exp),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
var value string
|
||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if value != test.exp {
|
||||
t.Fatalf("expected to have value %s, got: %s", test.exp, value)
|
||||
}
|
||||
@ -259,32 +234,29 @@ func TestKeyActionNode(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "input.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "input.html")
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
var nodes []*cdp.Node
|
||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes))
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, KeyActionNode(nodes[0], test.exp))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
var value string
|
||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
KeyActionNode(nodes[0], test.exp),
|
||||
Value(test.sel, &value, test.by),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if value != test.exp {
|
||||
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"
|
||||
"strings"
|
||||
|
||||
"github.com/chromedp/chromedp/kb"
|
||||
"git.loafle.net/commons_go/chromedp/kb"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -72,8 +72,6 @@ func main() {
|
||||
}
|
||||
|
||||
func run() error {
|
||||
var err error
|
||||
|
||||
// special characters
|
||||
keys := map[rune]kb.Key{
|
||||
'\b': {"Backspace", "Backspace", "", "", int64('\b'), int64('\b'), false, false},
|
||||
@ -82,8 +80,7 @@ func run() error {
|
||||
}
|
||||
|
||||
// load keys
|
||||
err = loadKeys(keys)
|
||||
if err != nil {
|
||||
if err := loadKeys(keys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -94,24 +91,19 @@ func run() error {
|
||||
}
|
||||
|
||||
// output
|
||||
err = ioutil.WriteFile(
|
||||
*flagOut,
|
||||
if err := ioutil.WriteFile(*flagOut,
|
||||
[]byte(fmt.Sprintf(hdr, *flagPkg, string(constBuf), string(mapBuf))),
|
||||
0644,
|
||||
)
|
||||
if err != nil {
|
||||
0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// format
|
||||
err = exec.Command("goimports", "-w", *flagOut).Run()
|
||||
if err != nil {
|
||||
if err := exec.Command("goimports", "-w", *flagOut).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// format
|
||||
err = exec.Command("gofmt", "-s", "-w", *flagOut).Run()
|
||||
if err != nil {
|
||||
if err := exec.Command("gofmt", "-s", "-w", *flagOut).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -120,8 +112,6 @@ func run() error {
|
||||
|
||||
// loadKeys loads the dom key definitions from the chromium source tree.
|
||||
func loadKeys(keys map[rune]kb.Key) error {
|
||||
var err error
|
||||
|
||||
// load key converter data
|
||||
keycodeConverterMap, err := loadKeycodeConverterData()
|
||||
if err != nil {
|
||||
@ -444,8 +434,6 @@ var defineRE = regexp.MustCompile(`(?m)^#define\s+(.+?)\s+([0-9A-Fx]+)`)
|
||||
// loadPosixWinKeyboardCodes loads the native and windows keyboard scan codes
|
||||
// mapped to the DOM key.
|
||||
func loadPosixWinKeyboardCodes() (map[string][]int64, error) {
|
||||
var err error
|
||||
|
||||
lookup := map[string]string{
|
||||
// mac alias
|
||||
"VKEY_LWIN": "0x5B",
|
||||
|
39
nav.go
39
nav.go
@ -10,18 +10,9 @@ import (
|
||||
|
||||
// Navigate navigates the current frame.
|
||||
func Navigate(urlstr string) Action {
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
th, ok := h.(*TargetHandler)
|
||||
if !ok {
|
||||
return ErrInvalidHandler
|
||||
}
|
||||
|
||||
frameID, _, _, err := page.Navigate(urlstr).Do(ctxt, th)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return th.SetActive(ctxt, frameID)
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
_, _, _, err := page.Navigate(urlstr).Do(ctx, h)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@ -32,9 +23,9 @@ func NavigationEntries(currentIndex *int64, entries *[]*page.NavigationEntry) Ac
|
||||
panic("currentIndex and entries cannot be nil")
|
||||
}
|
||||
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
var err error
|
||||
*currentIndex, *entries, err = page.GetNavigationHistory().Do(ctxt, h)
|
||||
*currentIndex, *entries, err = page.GetNavigationHistory().Do(ctx, h)
|
||||
return err
|
||||
})
|
||||
}
|
||||
@ -47,8 +38,8 @@ func NavigateToHistoryEntry(entryID int64) Action {
|
||||
|
||||
// NavigateBack navigates the current frame backwards in its history.
|
||||
func NavigateBack() Action {
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
cur, entries, err := page.GetNavigationHistory().Do(ctxt, h)
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
cur, entries, err := page.GetNavigationHistory().Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -57,14 +48,14 @@ func NavigateBack() Action {
|
||||
return errors.New("invalid navigation entry")
|
||||
}
|
||||
|
||||
return page.NavigateToHistoryEntry(entries[cur-1].ID).Do(ctxt, h)
|
||||
return page.NavigateToHistoryEntry(entries[cur-1].ID).Do(ctx, h)
|
||||
})
|
||||
}
|
||||
|
||||
// NavigateForward navigates the current frame forwards in its history.
|
||||
func NavigateForward() Action {
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
cur, entries, err := page.GetNavigationHistory().Do(ctxt, h)
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
cur, entries, err := page.GetNavigationHistory().Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -73,7 +64,7 @@ func NavigateForward() Action {
|
||||
return errors.New("invalid navigation entry")
|
||||
}
|
||||
|
||||
return page.NavigateToHistoryEntry(entries[cur+1].ID).Do(ctxt, h)
|
||||
return page.NavigateToHistoryEntry(entries[cur+1].ID).Do(ctx, h)
|
||||
})
|
||||
}
|
||||
|
||||
@ -95,9 +86,9 @@ func CaptureScreenshot(res *[]byte) Action {
|
||||
panic("res cannot be nil")
|
||||
}
|
||||
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
var err error
|
||||
*res, err = page.CaptureScreenshot().Do(ctxt, h)
|
||||
*res, err = page.CaptureScreenshot().Do(ctx, h)
|
||||
return err
|
||||
})
|
||||
}
|
||||
@ -108,9 +99,9 @@ func CaptureScreenshot(res *[]byte) Action {
|
||||
panic("id cannot be nil")
|
||||
}
|
||||
|
||||
return ActionFunc(func(ctxt context.Context, h cdp.Executor) error {
|
||||
return ActionFunc(func(ctx context.Context, h cdp.Executor) error {
|
||||
var err error
|
||||
*id, err = page.AddScriptToEvaluateOnLoad(source).Do(ctxt, h)
|
||||
*id, err = page.AddScriptToEvaluateOnLoad(source).Do(ctx, h)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
423
nav_test.go
423
nav_test.go
@ -1,47 +1,43 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/png"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
)
|
||||
|
||||
func TestNavigate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
|
||||
expurl, exptitle := testdataDir+"/image.html", "this is title"
|
||||
|
||||
err = c.Run(defaultContext, Navigate(expurl))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, WaitVisible(`#icon-brankas`, ByID))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
var urlstr string
|
||||
err = c.Run(defaultContext, Location(&urlstr))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
WaitVisible(`#icon-brankas`, ByID),
|
||||
Location(&urlstr),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(urlstr, expurl) {
|
||||
if !strings.HasSuffix(urlstr, "image.html") {
|
||||
t.Errorf("expected to be on image.html, at: %s", urlstr)
|
||||
}
|
||||
|
||||
var title string
|
||||
err = c.Run(defaultContext, Title(&title))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Title(&title)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
exptitle := "this is title"
|
||||
if title != exptitle {
|
||||
t.Errorf("expected title to contain google, instead title is: %s", title)
|
||||
}
|
||||
@ -50,21 +46,19 @@ func TestNavigate(t *testing.T) {
|
||||
func TestNavigationEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
ctx, cancel := testAllocate(t, "")
|
||||
defer cancel()
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
|
||||
tests := []string{
|
||||
"form.html",
|
||||
"image.html",
|
||||
tests := []struct {
|
||||
file, waitID string
|
||||
}{
|
||||
{"form.html", "#form"},
|
||||
{"image.html", "#icon-brankas"},
|
||||
}
|
||||
|
||||
var entries []*page.NavigationEntry
|
||||
var index int64
|
||||
|
||||
err = c.Run(defaultContext, NavigationEntries(&index, &entries))
|
||||
if err != nil {
|
||||
if err := Run(ctx, NavigationEntries(&index, &entries)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -76,24 +70,19 @@ func TestNavigationEntries(t *testing.T) {
|
||||
}
|
||||
|
||||
expIdx, expEntries := 1, 2
|
||||
for i, url := range tests {
|
||||
err = c.Run(defaultContext, Navigate(testdataDir+"/"+url))
|
||||
if err != nil {
|
||||
for i, test := range tests {
|
||||
if err := Run(ctx,
|
||||
Navigate(testdataDir+"/"+test.file),
|
||||
WaitVisible(test.waitID, ByID),
|
||||
NavigationEntries(&index, &entries),
|
||||
); err != nil {
|
||||
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 {
|
||||
t.Errorf("test %d expected to have %d navigation entry: got %d", i, expEntries, len(entries))
|
||||
}
|
||||
if index != int64(i+1) {
|
||||
t.Errorf("test %d expected navigation index is %d, got: %d", i, i, index)
|
||||
if want := int64(i + 1); index != want {
|
||||
t.Errorf("test %d expected navigation index is %d, got: %d", i, want, index)
|
||||
}
|
||||
|
||||
expIdx++
|
||||
@ -104,42 +93,27 @@ func TestNavigationEntries(t *testing.T) {
|
||||
func TestNavigateToHistoryEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
var entries []*page.NavigationEntry
|
||||
var index int64
|
||||
err = c.Run(defaultContext, Navigate(testdataDir+"/image.html"))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||
NavigationEntries(&index, &entries),
|
||||
|
||||
Navigate(testdataDir+"/form.html"),
|
||||
WaitVisible(`#form`, ByID), // for form.html
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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
|
||||
err = c.Run(defaultContext, Title(&title))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
NavigateToHistoryEntry(entries[index].ID),
|
||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||
Title(&title),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if title != entries[index].Title {
|
||||
@ -150,43 +124,24 @@ func TestNavigateToHistoryEntry(t *testing.T) {
|
||||
func TestNavigateBack(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
var title, exptitle string
|
||||
if err := Run(ctx,
|
||||
WaitVisible(`#form`, ByID), // for form.html
|
||||
Title(&exptitle),
|
||||
|
||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
||||
if err != nil {
|
||||
Navigate(testdataDir+"/image.html"),
|
||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||
|
||||
NavigateBack(),
|
||||
WaitVisible(`#form`, ByID), // for form.html
|
||||
Title(&title),
|
||||
); 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, 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 {
|
||||
t.Errorf("expected title to be %s, instead title is: %s", exptitle, title)
|
||||
}
|
||||
@ -195,50 +150,27 @@ func TestNavigateBack(t *testing.T) {
|
||||
func TestNavigateForward(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
var title, exptitle string
|
||||
if err := Run(ctx,
|
||||
WaitVisible(`#form`, ByID), // for form.html
|
||||
|
||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
||||
if err != nil {
|
||||
Navigate(testdataDir+"/image.html"),
|
||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||
Title(&exptitle),
|
||||
|
||||
NavigateBack(),
|
||||
WaitVisible(`#form`, ByID), // for form.html
|
||||
|
||||
NavigateForward(),
|
||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||
Title(&title),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Errorf("expected title to be %s, instead title is: %s", exptitle, title)
|
||||
}
|
||||
@ -247,18 +179,9 @@ func TestNavigateForward(t *testing.T) {
|
||||
func TestStop(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
|
||||
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 {
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
if err := Run(ctx, Stop()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -266,36 +189,38 @@ func TestStop(t *testing.T) {
|
||||
func TestReload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
count := 0
|
||||
// create test server
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(res, `<html>
|
||||
<head>
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="count%d"></div>
|
||||
</body></html`, count)
|
||||
count++
|
||||
})
|
||||
s := httptest.NewServer(mux)
|
||||
defer s.Close()
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "")
|
||||
defer cancel()
|
||||
|
||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
||||
if err != nil {
|
||||
var title, exptitle string
|
||||
if err := Run(ctx,
|
||||
Navigate(s.URL),
|
||||
WaitReady(`#count0`, ByID),
|
||||
Title(&exptitle),
|
||||
|
||||
Reload(),
|
||||
WaitReady(`#count1`, ByID),
|
||||
Title(&title),
|
||||
); 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, 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 {
|
||||
t.Errorf("expected title to be %s, instead title is: %s", exptitle, title)
|
||||
}
|
||||
@ -304,51 +229,47 @@ func TestReload(t *testing.T) {
|
||||
func TestCaptureScreenshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
|
||||
err = c.Run(defaultContext, Navigate(testdataDir+"/image.html"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
// set the viewport size, to know what screenshot size to expect
|
||||
width, height := 650, 450
|
||||
var buf []byte
|
||||
err = c.Run(defaultContext, CaptureScreenshot(&buf))
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
t.Fatal("failed to capture screenshot")
|
||||
config, format, err := image.DecodeConfig(bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := "png"; format != want {
|
||||
t.Fatalf("expected format to be %q, got %q", want, format)
|
||||
}
|
||||
if config.Width != width || config.Height != height {
|
||||
t.Fatalf("expected dimensions to be %d*%d, got %d*%d",
|
||||
width, height, config.Width, config.Height)
|
||||
}
|
||||
//TODO: test image
|
||||
}
|
||||
|
||||
/*func TestAddOnLoadScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "")
|
||||
defer cancel()
|
||||
|
||||
var scriptID page.ScriptIdentifier
|
||||
err = c.Run(defaultContext, AddOnLoadScript(`window.alert("TEST")`, &scriptID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
AddOnLoadScript(`window.alert("TEST")`, &scriptID),
|
||||
Navigate(testdataDir+"/form.html"),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, Navigate(testdataDir+"/form.html"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if scriptID == "" {
|
||||
t.Fatal("got empty script ID")
|
||||
}
|
||||
@ -358,57 +279,40 @@ func TestCaptureScreenshot(t *testing.T) {
|
||||
func TestRemoveOnLoadScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "")
|
||||
defer cancel()
|
||||
|
||||
var scriptID page.ScriptIdentifier
|
||||
err = c.Run(defaultContext, AddOnLoadScript(`window.alert("TEST")`, &scriptID))
|
||||
if err != nil {
|
||||
if err := Run(ctx, AddOnLoadScript(`window.alert("TEST")`, &scriptID)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if scriptID == "" {
|
||||
t.Fatal("got empty script ID")
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, RemoveOnLoadScript(scriptID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
RemoveOnLoadScript(scriptID),
|
||||
Navigate(testdataDir+"/form.html"),
|
||||
); err != nil {
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
expurl := testdataDir + "/form.html"
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
|
||||
err = c.Run(defaultContext, Navigate(expurl))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
var urlstr string
|
||||
err = c.Run(defaultContext, Location(&urlstr))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
WaitVisible(`#form`, ByID), // for form.html
|
||||
Location(&urlstr),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if urlstr != expurl {
|
||||
if !strings.HasSuffix(urlstr, "form.html") {
|
||||
t.Fatalf("expected to be on form.html, got: %s", urlstr)
|
||||
}
|
||||
}
|
||||
@ -416,26 +320,35 @@ func TestLocation(t *testing.T) {
|
||||
func TestTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
expurl, exptitle := testdataDir+"/image.html", "this is title"
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
|
||||
err = c.Run(defaultContext, Navigate(expurl))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
var title string
|
||||
err = c.Run(defaultContext, Title(&title))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
WaitVisible(`#icon-brankas`, ByID), // for image.html
|
||||
Title(&title),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
exptitle := "this is title"
|
||||
if title != exptitle {
|
||||
t.Fatalf("expected title to be %s, got: %s", exptitle, title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadIframe(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := testAllocate(t, "iframe.html")
|
||||
defer cancel()
|
||||
|
||||
if err := Run(ctx, Tasks{
|
||||
// TODO: remove the sleep once we have better support for
|
||||
// iframes.
|
||||
Sleep(10 * time.Millisecond),
|
||||
// WaitVisible(`#form`, ByID), // for the nested form.html
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
219
pool.go
219
pool.go
@ -1,219 +0,0 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/chromedp/chromedp/runner"
|
||||
)
|
||||
|
||||
// Pool manages a pool of running Chrome processes.
|
||||
type Pool struct {
|
||||
// start is the start port.
|
||||
start int
|
||||
|
||||
// end is the end port.
|
||||
end int
|
||||
|
||||
// res are the running chrome resources.
|
||||
res map[int]*Res
|
||||
|
||||
// logging funcs
|
||||
logf, debugf, errf func(string, ...interface{})
|
||||
|
||||
rw sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPool creates a new Chrome runner pool.
|
||||
func NewPool(opts ...PoolOption) (*Pool, error) {
|
||||
p := &Pool{
|
||||
start: DefaultPoolStartPort,
|
||||
end: DefaultPoolEndPort,
|
||||
res: make(map[int]*Res),
|
||||
logf: log.Printf,
|
||||
debugf: func(string, ...interface{}) {},
|
||||
}
|
||||
|
||||
// apply opts
|
||||
for _, o := range opts {
|
||||
if err := o(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if p.errf == nil {
|
||||
p.errf = func(s string, v ...interface{}) {
|
||||
p.logf("ERROR: "+s, v...)
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Shutdown releases all the pool resources.
|
||||
func (p *Pool) Shutdown() error {
|
||||
p.rw.Lock()
|
||||
defer p.rw.Unlock()
|
||||
|
||||
for _, r := range p.res {
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Allocate creates a new process runner and returns it.
|
||||
func (p *Pool) Allocate(ctxt context.Context, opts ...runner.CommandLineOption) (*Res, error) {
|
||||
var err error
|
||||
|
||||
r := p.next(ctxt)
|
||||
|
||||
// Check if the port is available first. If it's not, Chrome will print
|
||||
// an "address already in use" error, but it will otherwise keep
|
||||
// running. This can lead to Allocate succeeding, while the chrome
|
||||
// process isn't actually listening on the port we need.
|
||||
l, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", r.port))
|
||||
if err != nil {
|
||||
// we can't use this port, e.g. address already in use
|
||||
p.errf("pool could not allocate runner on port %d: %v", r.port, err)
|
||||
return nil, err
|
||||
}
|
||||
l.Close()
|
||||
|
||||
p.debugf("pool allocating %d", r.port)
|
||||
|
||||
// create runner
|
||||
r.r, err = runner.New(append([]runner.CommandLineOption{
|
||||
runner.ExecPath(runner.LookChromeNames("headless_shell")),
|
||||
runner.RemoteDebuggingPort(r.port),
|
||||
runner.NoDefaultBrowserCheck,
|
||||
runner.NoFirstRun,
|
||||
runner.Headless,
|
||||
}, opts...)...)
|
||||
if err != nil {
|
||||
defer r.Release()
|
||||
p.errf("pool could not allocate runner on port %d: %v", r.port, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// start runner
|
||||
err = r.r.Start(r.ctxt)
|
||||
if err != nil {
|
||||
defer r.Release()
|
||||
p.errf("pool could not start runner on port %d: %v", r.port, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// setup cdp
|
||||
r.c, err = New(
|
||||
r.ctxt, WithRunner(r.r),
|
||||
WithLogf(p.logf), WithDebugf(p.debugf), WithErrorf(p.errf),
|
||||
)
|
||||
if err != nil {
|
||||
defer r.Release()
|
||||
p.errf("pool could not connect to %d: %v", r.port, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// next returns the next available res.
|
||||
func (p *Pool) next(ctxt context.Context) *Res {
|
||||
p.rw.Lock()
|
||||
defer p.rw.Unlock()
|
||||
|
||||
var found bool
|
||||
var i int
|
||||
for i = p.start; i < p.end; i++ {
|
||||
if _, ok := p.res[i]; !ok {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
panic("no ports available")
|
||||
}
|
||||
|
||||
r := &Res{
|
||||
p: p,
|
||||
port: i,
|
||||
}
|
||||
r.ctxt, r.cancel = context.WithCancel(ctxt)
|
||||
|
||||
p.res[i] = r
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Res is a pool resource.
|
||||
type Res struct {
|
||||
p *Pool
|
||||
ctxt context.Context
|
||||
cancel func()
|
||||
port int
|
||||
r *runner.Runner
|
||||
c *CDP
|
||||
}
|
||||
|
||||
// Release releases the pool resource.
|
||||
func (r *Res) Release() error {
|
||||
r.cancel()
|
||||
|
||||
var err error
|
||||
if r.c != nil {
|
||||
err = r.c.Wait()
|
||||
}
|
||||
|
||||
defer r.p.debugf("pool released %d", r.port)
|
||||
|
||||
r.p.rw.Lock()
|
||||
defer r.p.rw.Unlock()
|
||||
delete(r.p.res, r.port)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Port returns the allocated port for the pool resource.
|
||||
func (r *Res) Port() int {
|
||||
return r.port
|
||||
}
|
||||
|
||||
// URL returns a formatted URL for the pool resource.
|
||||
func (r *Res) URL() string {
|
||||
return fmt.Sprintf("http://localhost:%d/json", r.port)
|
||||
}
|
||||
|
||||
// CDP returns the actual CDP instance.
|
||||
func (r *Res) CDP() *CDP {
|
||||
return r.c
|
||||
}
|
||||
|
||||
// Run runs an action.
|
||||
func (r *Res) Run(ctxt context.Context, a Action) error {
|
||||
return r.c.Run(ctxt, a)
|
||||
}
|
||||
|
||||
// PoolOption is a pool option.
|
||||
type PoolOption func(*Pool) error
|
||||
|
||||
// PortRange is a pool option to set the port range to use.
|
||||
func PortRange(start, end int) PoolOption {
|
||||
return func(p *Pool) error {
|
||||
p.start = start
|
||||
p.end = end
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PoolLog is a pool option to set the logging to use for the pool.
|
||||
func PoolLog(logf, debugf, errf func(string, ...interface{})) PoolOption {
|
||||
return func(p *Pool) error {
|
||||
p.logf, p.debugf, p.errf = logf, debugf, errf
|
||||
return nil
|
||||
}
|
||||
}
|
47
pool_test.go
47
pool_test.go
@ -1,47 +0,0 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllocatePortInUse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// take a random available port
|
||||
l, err := net.Listen("tcp4", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
ctxt, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// make the pool use the port already in use via a port range
|
||||
_, portStr, _ := net.SplitHostPort(l.Addr().String())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
pool, err := NewPool(
|
||||
PortRange(port, port+1),
|
||||
// skip the error log from the used port
|
||||
PoolLog(nil, nil, func(string, ...interface{}) {}),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c, err := pool.Allocate(ctxt)
|
||||
if err != nil {
|
||||
want := "address already in use"
|
||||
got := err.Error()
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("wanted error to contain %q, but got %q", want, got)
|
||||
}
|
||||
} else {
|
||||
t.Fatal("wanted Allocate to error if port is in use")
|
||||
c.Release()
|
||||
}
|
||||
}
|
99
query.go
99
query.go
@ -25,7 +25,7 @@ func Nodes(sel interface{}, nodes *[]*cdp.Node, opts ...QueryOption) Action {
|
||||
panic("nodes cannot be nil")
|
||||
}
|
||||
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, n ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, n ...*cdp.Node) error {
|
||||
*nodes = n
|
||||
return nil
|
||||
}, opts...)
|
||||
@ -37,7 +37,7 @@ func NodeIDs(sel interface{}, ids *[]cdp.NodeID, opts ...QueryOption) Action {
|
||||
panic("nodes cannot be nil")
|
||||
}
|
||||
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
nodeIDs := make([]cdp.NodeID, len(nodes))
|
||||
for i, n := range nodes {
|
||||
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.
|
||||
func Focus(sel interface{}, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
return dom.Focus().WithNodeID(nodes[0].NodeID).Do(ctxt, h)
|
||||
return dom.Focus().WithNodeID(nodes[0].NodeID).Do(ctx, h)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
// Blur unfocuses (blurs) the first node matching the selector.
|
||||
func Blur(sel interface{}, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
var res bool
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(blurJS, nodes[0].FullXPath()), &res).Do(ctxt, h)
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(blurJS, nodes[0].FullXPath()), &res).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -87,12 +87,12 @@ func Dimensions(sel interface{}, model **dom.BoxModel, opts ...QueryOption) Acti
|
||||
if model == nil {
|
||||
panic("model cannot be nil")
|
||||
}
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
var err error
|
||||
*model, err = dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctxt, h)
|
||||
*model, err = dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctx, h)
|
||||
return err
|
||||
}, opts...)
|
||||
}
|
||||
@ -103,18 +103,18 @@ func Text(sel interface{}, text *string, opts ...QueryOption) Action {
|
||||
panic("text cannot be nil")
|
||||
}
|
||||
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
return EvaluateAsDevTools(fmt.Sprintf(textJS, nodes[0].FullXPath()), text).Do(ctxt, h)
|
||||
return EvaluateAsDevTools(fmt.Sprintf(textJS, nodes[0].FullXPath()), text).Do(ctx, h)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
// Clear clears the values of any input/textarea nodes matching the selector.
|
||||
func Clear(sel interface{}, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
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, "")
|
||||
}
|
||||
errs[i] = a.Do(ctxt, h)
|
||||
errs[i] = a.Do(ctx, h)
|
||||
}(i, n)
|
||||
}
|
||||
wg.Wait()
|
||||
@ -190,7 +190,7 @@ func Attributes(sel interface{}, attributes *map[string]string, opts ...QueryOpt
|
||||
panic("attributes cannot be nil")
|
||||
}
|
||||
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
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")
|
||||
}
|
||||
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
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
|
||||
// selector.
|
||||
func SetAttributes(sel interface{}, attributes map[string]string, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return errors.New("expected at least one element")
|
||||
}
|
||||
@ -254,7 +254,7 @@ func SetAttributes(sel interface{}, attributes map[string]string, opts ...QueryO
|
||||
i++
|
||||
}
|
||||
|
||||
return dom.SetAttributesAsText(nodes[0].NodeID, strings.Join(attrs, " ")).Do(ctxt, h)
|
||||
return dom.SetAttributesAsText(nodes[0].NodeID, strings.Join(attrs, " ")).Do(ctx, h)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
@ -265,7 +265,7 @@ func AttributeValue(sel interface{}, name string, value *string, ok *bool, opts
|
||||
panic("value cannot be nil")
|
||||
}
|
||||
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
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
|
||||
// first node matching the selector.
|
||||
func SetAttributeValue(sel interface{}, name, value string, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
return dom.SetAttributeValue(nodes[0].NodeID, name, value).Do(ctxt, h)
|
||||
return dom.SetAttributeValue(nodes[0].NodeID, name, value).Do(ctx, h)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
// RemoveAttribute removes the element attribute with name from the first node
|
||||
// matching the selector.
|
||||
func RemoveAttribute(sel interface{}, name string, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
return dom.RemoveAttribute(nodes[0].NodeID, name).Do(ctxt, h)
|
||||
return dom.RemoveAttribute(nodes[0].NodeID, name).Do(ctx, h)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
@ -322,25 +322,25 @@ func JavascriptAttribute(sel interface{}, name string, res interface{}, opts ...
|
||||
if res == nil {
|
||||
panic("res cannot be nil")
|
||||
}
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
return EvaluateAsDevTools(fmt.Sprintf(attributeJS, nodes[0].FullXPath(), name), res).Do(ctxt, h)
|
||||
return EvaluateAsDevTools(fmt.Sprintf(attributeJS, nodes[0].FullXPath(), name), res).Do(ctx, h)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
// SetJavascriptAttribute sets the javascript attribute for the first node
|
||||
// matching the selector.
|
||||
func SetJavascriptAttribute(sel interface{}, name, value string, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
var res string
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(setAttributeJS, nodes[0].FullXPath(), name, value), &res).Do(ctxt, h)
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(setAttributeJS, nodes[0].FullXPath(), name, value), &res).Do(ctx, h)
|
||||
if err != nil {
|
||||
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.
|
||||
func Click(sel interface{}, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
return MouseClickNode(nodes[0]).Do(ctxt, h)
|
||||
return MouseClickNode(nodes[0]).Do(ctx, h)
|
||||
}, append(opts, NodeVisible)...)
|
||||
}
|
||||
|
||||
// DoubleClick sends a mouse double click event to the first node matching the
|
||||
// selector.
|
||||
func DoubleClick(sel interface{}, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
return MouseClickNode(nodes[0], ClickCount(2)).Do(ctxt, h)
|
||||
return MouseClickNode(nodes[0], ClickCount(2)).Do(ctx, h)
|
||||
}, append(opts, NodeVisible)...)
|
||||
}
|
||||
|
||||
@ -397,7 +397,7 @@ func DoubleClick(sel interface{}, opts ...QueryOption) Action {
|
||||
// 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.
|
||||
func SendKeys(sel interface{}, v string, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
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
|
||||
if n.NodeName == "INPUT" && typ == "file" {
|
||||
return dom.SetFileInputFiles([]string{v}).WithNodeID(n.NodeID).Do(ctxt, h)
|
||||
return dom.SetFileInputFiles([]string{v}).WithNodeID(n.NodeID).Do(ctx, h)
|
||||
}
|
||||
|
||||
return KeyActionNode(n, v).Do(ctxt, h)
|
||||
return KeyActionNode(n, v).Do(ctx, h)
|
||||
}, append(opts, NodeVisible)...)
|
||||
}
|
||||
|
||||
// SetUploadFiles sets the files to upload (ie, for a input[type="file"] node)
|
||||
// for the first node matching the selector.
|
||||
func SetUploadFiles(sel interface{}, files []string, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
return dom.SetFileInputFiles(files).WithNodeID(nodes[0].NodeID).Do(ctxt, h)
|
||||
return dom.SetFileInputFiles(files).WithNodeID(nodes[0].NodeID).Do(ctx, h)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
@ -441,13 +441,13 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
|
||||
panic("picbuf cannot be nil")
|
||||
}
|
||||
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
// get box model
|
||||
box, err := dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctxt, h)
|
||||
box, err := dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -459,13 +459,13 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
|
||||
|
||||
// scroll to node position
|
||||
var pos []int
|
||||
err = EvaluateAsDevTools(fmt.Sprintf(scrollJS, int64(box.Margin[0]), int64(box.Margin[1])), &pos).Do(ctxt, h)
|
||||
err = EvaluateAsDevTools(fmt.Sprintf(scrollJS, int64(box.Margin[0]), int64(box.Margin[1])), &pos).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// take page screenshot
|
||||
buf, err := page.CaptureScreenshot().Do(ctxt, h)
|
||||
buf, err := page.CaptureScreenshot().Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -484,8 +484,7 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
|
||||
|
||||
// encode
|
||||
var croppedBuf bytes.Buffer
|
||||
err = png.Encode(&croppedBuf, cropped)
|
||||
if err != nil {
|
||||
if err := png.Encode(&croppedBuf, cropped); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -498,13 +497,13 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action {
|
||||
// Submit is an action that submits the form of the first node matching the
|
||||
// selector belongs to.
|
||||
func Submit(sel interface{}, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
var res bool
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(submitJS, nodes[0].FullXPath()), &res).Do(ctxt, h)
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(submitJS, nodes[0].FullXPath()), &res).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -520,13 +519,13 @@ func Submit(sel interface{}, opts ...QueryOption) Action {
|
||||
// Reset is an action that resets the form of the first node matching the
|
||||
// selector belongs to.
|
||||
func Reset(sel interface{}, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
var res bool
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(resetJS, nodes[0].FullXPath()), &res).Do(ctxt, h)
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(resetJS, nodes[0].FullXPath()), &res).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -545,12 +544,12 @@ func ComputedStyle(sel interface{}, style *[]*css.ComputedProperty, opts ...Quer
|
||||
panic("style cannot be nil")
|
||||
}
|
||||
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
computed, err := css.GetComputedStyleForNode(nodes[0].NodeID).Do(ctxt, h)
|
||||
computed, err := css.GetComputedStyleForNode(nodes[0].NodeID).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -568,7 +567,7 @@ func MatchedStyle(sel interface{}, style **css.GetMatchedStylesForNodeReturns, o
|
||||
panic("style cannot be nil")
|
||||
}
|
||||
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
@ -577,7 +576,7 @@ func MatchedStyle(sel interface{}, style **css.GetMatchedStylesForNodeReturns, o
|
||||
ret := &css.GetMatchedStylesForNodeReturns{}
|
||||
ret.InlineStyle, ret.AttributesStyle, ret.MatchedCSSRules,
|
||||
ret.PseudoElements, ret.Inherited, ret.CSSKeyframesRules,
|
||||
err = css.GetMatchedStylesForNode(nodes[0].NodeID).Do(ctxt, h)
|
||||
err = css.GetMatchedStylesForNode(nodes[0].NodeID).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -590,13 +589,13 @@ func MatchedStyle(sel interface{}, style **css.GetMatchedStylesForNodeReturns, o
|
||||
|
||||
// ScrollIntoView scrolls the window to the first node matching the selector.
|
||||
func ScrollIntoView(sel interface{}, opts ...QueryOption) Action {
|
||||
return QueryAfter(sel, func(ctxt context.Context, h *TargetHandler, nodes ...*cdp.Node) error {
|
||||
return QueryAfter(sel, func(ctx context.Context, h *Target, nodes ...*cdp.Node) error {
|
||||
if len(nodes) < 1 {
|
||||
return fmt.Errorf("selector `%s` did not return any nodes", sel)
|
||||
}
|
||||
|
||||
var pos []int
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, nodes[0].FullXPath()), &pos).Do(ctxt, h)
|
||||
err := EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, nodes[0].FullXPath()), &pos).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
508
query_test.go
508
query_test.go
@ -1,7 +1,10 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/png"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@ -13,15 +16,16 @@ import (
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/css"
|
||||
"github.com/chromedp/cdproto/dom"
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
|
||||
"github.com/chromedp/chromedp/kb"
|
||||
"git.loafle.net/commons_go/chromedp/kb"
|
||||
)
|
||||
|
||||
func TestNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "table.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "table.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -34,13 +38,12 @@ func TestNodes(t *testing.T) {
|
||||
{"#footer", ByID, 1},
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var nodes []*cdp.Node
|
||||
err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
if len(nodes) != test.len {
|
||||
t.Errorf("test %d expected to have %d nodes: got %d", i, test.len, len(nodes))
|
||||
}
|
||||
@ -50,8 +53,8 @@ func TestNodes(t *testing.T) {
|
||||
func TestNodeIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "table.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "table.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -64,13 +67,12 @@ func TestNodeIDs(t *testing.T) {
|
||||
{"#footer", ByID, 1},
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var ids []cdp.NodeID
|
||||
err = c.Run(defaultContext, NodeIDs(test.sel, &ids, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, NodeIDs(test.sel, &ids, test.by)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(ids) != test.len {
|
||||
t.Errorf("test %d expected to have %d node id's: got %d", i, test.len, len(ids))
|
||||
}
|
||||
@ -80,8 +82,8 @@ func TestNodeIDs(t *testing.T) {
|
||||
func TestFocusBlur(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -93,35 +95,29 @@ func TestFocusBlur(t *testing.T) {
|
||||
{"#input1", ByID},
|
||||
}
|
||||
|
||||
err := c.Run(defaultContext, Click("#input1", ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Click("#input1", ByID)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
err = c.Run(defaultContext, Focus(test.sel, test.by))
|
||||
if err != nil {
|
||||
var value string
|
||||
if err := Run(ctx,
|
||||
Focus(test.sel, test.by),
|
||||
Value(test.sel, &value, test.by),
|
||||
); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
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" {
|
||||
t.Errorf("test %d expected value is '9999', got: '%s'", i, value)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, Blur(test.sel, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
Blur(test.sel, test.by),
|
||||
Value(test.sel, &value, test.by),
|
||||
); err != nil {
|
||||
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" {
|
||||
t.Errorf("test %d expected value is '0', got: '%s'", i, value)
|
||||
}
|
||||
@ -131,8 +127,8 @@ func TestFocusBlur(t *testing.T) {
|
||||
func TestDimensions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "image.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -146,13 +142,12 @@ func TestDimensions(t *testing.T) {
|
||||
{"#icon-github", ByID, 120, 120},
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var model *dom.BoxModel
|
||||
err = c.Run(defaultContext, Dimensions(test.sel, &model))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Dimensions(test.sel, &model)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@ -162,8 +157,8 @@ func TestDimensions(t *testing.T) {
|
||||
func TestText(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "form.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -176,13 +171,12 @@ func TestText(t *testing.T) {
|
||||
{"/html/body/form/span[2]", BySearch, "keyword"},
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var text string
|
||||
err = c.Run(defaultContext, Text(test.sel, &text, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Text(test.sel, &text, test.by)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
if text != test.exp {
|
||||
t.Errorf("test %d expected `%s`, got: %s", i, test.exp, text)
|
||||
}
|
||||
@ -214,28 +208,24 @@ func TestClear(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "form.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
var val string
|
||||
err := c.Run(defaultContext, Value(test.sel, &val, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Value(test.sel, &val, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if val == "" {
|
||||
t.Errorf("expected `%s` to have non empty value", test.sel)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, Clear(test.sel, test.by))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, Value(test.sel, &val, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
Clear(test.sel, test.by),
|
||||
Value(test.sel, &val, test.by),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if val != "" {
|
||||
@ -261,27 +251,22 @@ func TestReset(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "form.html")
|
||||
defer c.Release()
|
||||
|
||||
err := c.Run(defaultContext, SetValue(test.sel, test.value, test.by))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, Reset(test.sel, test.by))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
var value string
|
||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
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)
|
||||
}
|
||||
|
||||
if value != test.exp {
|
||||
t.Errorf("expected value after reset is %s, got: '%s'", test.exp, value)
|
||||
}
|
||||
@ -292,8 +277,8 @@ func TestReset(t *testing.T) {
|
||||
func TestValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "form.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -305,13 +290,12 @@ func TestValue(t *testing.T) {
|
||||
{`#keyword`, ByID},
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var value string
|
||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
if value != "chromedp" {
|
||||
t.Errorf("test %d expected `chromedp`, got: %s", i, value)
|
||||
}
|
||||
@ -332,22 +316,21 @@ func TestSetValue(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "form.html")
|
||||
defer c.Release()
|
||||
|
||||
err := c.Run(defaultContext, SetValue(test.sel, "FOOBAR", test.by))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
var value string
|
||||
err = c.Run(defaultContext, Value(test.sel, &value, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
SetValue(test.sel, "FOOBAR", test.by),
|
||||
Value(test.sel, &value, test.by),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if value != "FOOBAR" {
|
||||
t.Errorf("expected `FOOBAR`, got: %s", value)
|
||||
}
|
||||
@ -358,45 +341,51 @@ func TestSetValue(t *testing.T) {
|
||||
func TestAttributes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "image.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
by QueryOption
|
||||
exp map[string]string
|
||||
}{
|
||||
{`//*[@id="icon-brankas"]`, BySearch,
|
||||
{
|
||||
`//*[@id="icon-brankas"]`, BySearch,
|
||||
map[string]string{
|
||||
"alt": "Brankas - Easy Money Management",
|
||||
"id": "icon-brankas",
|
||||
"src": "images/brankas.png",
|
||||
}},
|
||||
{"body > img:first-child", ByQuery,
|
||||
},
|
||||
},
|
||||
{
|
||||
"body > img:first-child", ByQuery,
|
||||
map[string]string{
|
||||
"alt": "Brankas - Easy Money Management",
|
||||
"id": "icon-brankas",
|
||||
"src": "images/brankas.png",
|
||||
}},
|
||||
{"body > img:nth-child(2)", ByQueryAll,
|
||||
},
|
||||
},
|
||||
{
|
||||
"body > img:nth-child(2)", ByQueryAll,
|
||||
map[string]string{
|
||||
"alt": `How people build software`,
|
||||
"id": "icon-github",
|
||||
"src": "images/github.png",
|
||||
}},
|
||||
{"#icon-github", ByID,
|
||||
},
|
||||
},
|
||||
{
|
||||
"#icon-github", ByID,
|
||||
map[string]string{
|
||||
"alt": "How people build software",
|
||||
"id": "icon-github",
|
||||
"src": "images/github.png",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var attrs map[string]string
|
||||
err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Attributes(test.sel, &attrs, test.by)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
@ -409,15 +398,16 @@ func TestAttributes(t *testing.T) {
|
||||
func TestAttributesAll(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "image.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
by QueryOption
|
||||
exp []map[string]string
|
||||
}{
|
||||
{"img", ByQueryAll,
|
||||
{
|
||||
"img", ByQueryAll,
|
||||
[]map[string]string{
|
||||
{
|
||||
"alt": "Brankas - Easy Money Management",
|
||||
@ -433,11 +423,9 @@ func TestAttributesAll(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var attrs []map[string]string
|
||||
err = c.Run(defaultContext, AttributesAll(test.sel, &attrs, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, AttributesAll(test.sel, &attrs, test.by)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
@ -456,22 +444,28 @@ func TestSetAttributes(t *testing.T) {
|
||||
attrs map[string]string
|
||||
exp map[string]string
|
||||
}{
|
||||
{`//*[@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,
|
||||
{
|
||||
`//*[@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: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{
|
||||
"alt": `How people build software`,
|
||||
@ -479,8 +473,10 @@ func TestSetAttributes(t *testing.T) {
|
||||
"src": "images/github.png",
|
||||
"width": "100",
|
||||
"height": "200",
|
||||
}},
|
||||
{"#icon-github", ByID,
|
||||
},
|
||||
},
|
||||
{
|
||||
"#icon-github", ByID,
|
||||
map[string]string{"width": "100", "height": "200"},
|
||||
map[string]string{
|
||||
"alt": "How people build software",
|
||||
@ -488,24 +484,27 @@ func TestSetAttributes(t *testing.T) {
|
||||
"src": "images/github.png",
|
||||
"width": "100",
|
||||
"height": "200",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "image.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
err := c.Run(defaultContext, SetAttributes(test.sel, test.attrs, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, SetAttributes(test.sel, test.attrs, test.by)); err != nil {
|
||||
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
|
||||
err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Attributes(test.sel, &attrs, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
@ -519,8 +518,8 @@ func TestSetAttributes(t *testing.T) {
|
||||
func TestAttributeValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "image.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -534,20 +533,15 @@ func TestAttributeValue(t *testing.T) {
|
||||
{"#icon-github", ByID, "alt", "How people build software"},
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var value string
|
||||
var ok bool
|
||||
|
||||
err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, AttributeValue(test.sel, test.attr, &value, &ok, test.by)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("test %d failed to get attribute %s on %s", i, test.attr, test.sel)
|
||||
}
|
||||
|
||||
if value != test.exp {
|
||||
t.Errorf("test %d expected %s to be %s, got: %s", i, test.attr, test.exp, value)
|
||||
}
|
||||
@ -570,27 +564,28 @@ func TestSetAttributeValue(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "form.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
err := c.Run(defaultContext, SetAttributeValue(test.sel, test.attr, test.exp, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, SetAttributeValue(test.sel, test.attr, test.exp, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
// TODO: figure why this test is flaky without this
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
var value string
|
||||
var ok bool
|
||||
err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, AttributeValue(test.sel, test.attr, &value, &ok, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("failed to get attribute %s on %s", test.attr, test.sel)
|
||||
}
|
||||
|
||||
if value != test.exp {
|
||||
t.Errorf("expected %s to be %s, got: %s", test.attr, test.exp, value)
|
||||
}
|
||||
@ -613,21 +608,23 @@ func TestRemoveAttribute(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "image.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
err := c.Run(defaultContext, RemoveAttribute(test.sel, test.attr))
|
||||
if err != nil {
|
||||
if err := Run(ctx, RemoveAttribute(test.sel, test.attr)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
// TODO: figure why this test is flaky without this
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
var value string
|
||||
var ok bool
|
||||
err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, AttributeValue(test.sel, test.attr, &value, &ok, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if ok || value != "" {
|
||||
@ -651,27 +648,22 @@ func TestClick(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "form.html")
|
||||
defer c.Release()
|
||||
|
||||
err := c.Run(defaultContext, Click(test.sel, test.by))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, WaitVisible("#icon-brankas", ByID))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
var title string
|
||||
err = c.Run(defaultContext, Title(&title))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
Click(test.sel, test.by),
|
||||
WaitVisible("#icon-brankas", ByID),
|
||||
Title(&title),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if title != "this is title" {
|
||||
t.Errorf("expected title to be 'chromedp - Google Search', got: '%s'", title)
|
||||
}
|
||||
@ -693,24 +685,21 @@ func TestDoubleClick(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
|
||||
err := c.Run(defaultContext, DoubleClick(test.sel, test.by))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
var value string
|
||||
err = c.Run(defaultContext, Value("#input1", &value, ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
DoubleClick(test.sel, test.by),
|
||||
Value("#input1", &value, ByID),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if value != "1" {
|
||||
t.Errorf("expected value to be '1', got: '%s'", value)
|
||||
}
|
||||
@ -736,22 +725,21 @@ func TestSendKeys(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "visible.html")
|
||||
defer c.Release()
|
||||
|
||||
err := c.Run(defaultContext, SendKeys(test.sel, test.keys, test.by))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
ctx, cancel := testAllocate(t, "visible.html")
|
||||
defer cancel()
|
||||
|
||||
var val string
|
||||
err = c.Run(defaultContext, Value(test.sel, &val, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
SendKeys(test.sel, test.keys, test.by),
|
||||
Value(test.sel, &val, test.by),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if val != test.exp {
|
||||
t.Errorf("expected value %s, got: %s", test.exp, val)
|
||||
}
|
||||
@ -762,31 +750,47 @@ func TestSendKeys(t *testing.T) {
|
||||
func TestScreenshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "image.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
by QueryOption
|
||||
sel string
|
||||
by QueryOption
|
||||
size int
|
||||
}{
|
||||
{"/html/body/img", BySearch},
|
||||
{"img", ByQueryAll},
|
||||
{"img", ByQuery},
|
||||
{"#icon-github", ByID},
|
||||
{"/html/body/img", BySearch, 239},
|
||||
{"img", ByQueryAll, 239},
|
||||
{"#icon-github", ByID, 120},
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var buf []byte
|
||||
err = c.Run(defaultContext, Screenshot(test.sel, &buf))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Screenshot(test.sel, &buf)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
t.Fatalf("test %d failed to capture screenshot", i)
|
||||
}
|
||||
//TODO: test image
|
||||
config, format, err := image.DecodeConfig(bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := "png"; format != want {
|
||||
t.Fatalf("expected format to be %q, got %q", want, format)
|
||||
}
|
||||
if config.Width != test.size || config.Height != test.size {
|
||||
t.Fatalf("expected dimensions to be %d*%d, got %d*%d",
|
||||
test.size, test.size, config.Width, config.Height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -804,27 +808,22 @@ func TestSubmit(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "form.html")
|
||||
defer c.Release()
|
||||
|
||||
err := c.Run(defaultContext, Submit(test.sel, test.by))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, WaitVisible("#icon-brankas", ByID))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
ctx, cancel := testAllocate(t, "form.html")
|
||||
defer cancel()
|
||||
|
||||
var title string
|
||||
err = c.Run(defaultContext, Title(&title))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
Submit(test.sel, test.by),
|
||||
WaitVisible("#icon-brankas", ByID),
|
||||
Title(&title),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if title != "this is title" {
|
||||
t.Errorf("expected title to be 'this is title', got: '%s'", title)
|
||||
}
|
||||
@ -846,17 +845,15 @@ func TestComputedStyle(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
var styles []*css.ComputedProperty
|
||||
err := c.Run(defaultContext, ComputedStyle(test.sel, &styles, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, ComputedStyle(test.sel, &styles, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
@ -867,16 +864,10 @@ func TestComputedStyle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, Click("#input1", ByID))
|
||||
if 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 {
|
||||
if err := Run(ctx,
|
||||
Click("#input1", ByID),
|
||||
ComputedStyle(test.sel, &styles, test.by),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
@ -905,17 +896,15 @@ func TestMatchedStyle(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
test := test
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
var styles *css.GetMatchedStylesForNodeReturns
|
||||
err := c.Run(defaultContext, MatchedStyle(test.sel, &styles, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, MatchedStyle(test.sel, &styles, test.by)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
@ -930,7 +919,7 @@ func TestFileUpload(t *testing.T) {
|
||||
// create test server
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(res, uploadHTML)
|
||||
fmt.Fprintf(res, "%s", uploadHTML)
|
||||
})
|
||||
mux.HandleFunc("/upload", func(res http.ResponseWriter, req *http.Request) {
|
||||
f, _, err := req.FormFile("upload")
|
||||
@ -957,10 +946,11 @@ func TestFileUpload(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
if _, err = tmpfile.WriteString(uploadHTML); err != nil {
|
||||
defer tmpfile.Close()
|
||||
if _, err := tmpfile.WriteString(uploadHTML); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = tmpfile.Close(); err != nil {
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -971,25 +961,26 @@ func TestFileUpload(t *testing.T) {
|
||||
{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 {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
// TODO: refactor the test so the subtests can run in
|
||||
// parallel
|
||||
//t.Parallel()
|
||||
|
||||
c := testAllocate(t, "")
|
||||
defer c.Release()
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
ctx, cancel := testAllocate(t, "")
|
||||
defer cancel()
|
||||
|
||||
var result string
|
||||
err = c.Run(defaultContext, Tasks{
|
||||
if err := Run(ctx,
|
||||
Navigate(s.URL),
|
||||
test.a,
|
||||
Click(`input[name="submit"]`),
|
||||
Text(`#result`, &result, ByID, NodeVisible),
|
||||
})
|
||||
if err != nil {
|
||||
); err != nil {
|
||||
t.Fatalf("test %d expected no error, got: %v", i, err)
|
||||
}
|
||||
|
||||
if result != fmt.Sprintf("%d", len(uploadHTML)) {
|
||||
t.Errorf("test %d expected result to be %d, got: %s", i, len(uploadHTML), result)
|
||||
}
|
||||
@ -1000,8 +991,8 @@ func TestFileUpload(t *testing.T) {
|
||||
func TestInnerHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "table.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "table.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -1011,13 +1002,12 @@ func TestInnerHTML(t *testing.T) {
|
||||
{"thead", ByQueryAll},
|
||||
{"thead", ByQuery},
|
||||
}
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var html string
|
||||
err = c.Run(defaultContext, InnerHTML(test.sel, &html))
|
||||
if err != nil {
|
||||
if err := Run(ctx, InnerHTML(test.sel, &html)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
if html == "" {
|
||||
t.Fatalf("test %d: InnerHTML is empty", i)
|
||||
}
|
||||
@ -1027,8 +1017,8 @@ func TestInnerHTML(t *testing.T) {
|
||||
func TestOuterHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "table.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "table.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -1038,13 +1028,12 @@ func TestOuterHTML(t *testing.T) {
|
||||
{"thead tr", ByQueryAll},
|
||||
{"thead tr", ByQuery},
|
||||
}
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
var html string
|
||||
err = c.Run(defaultContext, OuterHTML(test.sel, &html))
|
||||
if err != nil {
|
||||
if err := Run(ctx, OuterHTML(test.sel, &html)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
if html == "" {
|
||||
t.Fatalf("test %d: OuterHTML is empty", i)
|
||||
}
|
||||
@ -1054,8 +1043,8 @@ func TestOuterHTML(t *testing.T) {
|
||||
func TestScrollIntoView(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "image.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "image.html")
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
sel string
|
||||
@ -1066,12 +1055,11 @@ func TestScrollIntoView(t *testing.T) {
|
||||
{"img", ByQuery},
|
||||
{"#icon-github", ByID},
|
||||
}
|
||||
var err error
|
||||
for i, test := range tests {
|
||||
err = c.Run(defaultContext, ScrollIntoView(test.sel, test.by))
|
||||
if err != nil {
|
||||
if err := Run(ctx, ScrollIntoView(test.sel, test.by)); err != nil {
|
||||
t.Fatalf("test %d got error: %v", i, err)
|
||||
}
|
||||
|
||||
// TODO test scroll event
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
// +build darwin
|
||||
|
||||
package runner
|
||||
|
||||
const (
|
||||
// DefaultChromePath is the default path to use for Chrome if the
|
||||
// executable is not in $PATH.
|
||||
DefaultChromePath = `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
|
||||
)
|
||||
|
||||
// DefaultChromeNames are the default Chrome executable names to look for in
|
||||
// $PATH.
|
||||
var DefaultChromeNames []string
|
@ -1,19 +0,0 @@
|
||||
// +build linux freebsd netbsd openbsd
|
||||
|
||||
package runner
|
||||
|
||||
const (
|
||||
// DefaultChromePath is the default path to use for Chrome if the
|
||||
// executable is not in $PATH.
|
||||
DefaultChromePath = "/usr/bin/google-chrome"
|
||||
)
|
||||
|
||||
// DefaultChromeNames are the default Chrome executable names to look for in
|
||||
// $PATH.
|
||||
var DefaultChromeNames = []string{
|
||||
"google-chrome",
|
||||
"chromium-browser",
|
||||
"chromium",
|
||||
"google-chrome-beta",
|
||||
"google-chrome-unstable",
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package runner
|
||||
|
||||
const (
|
||||
// DefaultChromePath is the default path to use for Chrome if the
|
||||
// executable is not in %PATH%.
|
||||
DefaultChromePath = `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
|
||||
)
|
||||
|
||||
// DefaultChromeNames are the default Chrome executable names to look for in
|
||||
// %PATH%.
|
||||
var DefaultChromeNames = []string{`chrome.exe`}
|
482
runner/runner.go
482
runner/runner.go
@ -1,482 +0,0 @@
|
||||
// Package runner provides a Chrome process runner.
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/chromedp/chromedp/client"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultUserDataDirPrefix is the default user data directory prefix.
|
||||
DefaultUserDataDirPrefix = "chromedp-runner.%d."
|
||||
)
|
||||
|
||||
// Error is a runner error.
|
||||
type Error string
|
||||
|
||||
// Error satisfies the error interface.
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
// Error values.
|
||||
const (
|
||||
// ErrAlreadyStarted is the already started error.
|
||||
ErrAlreadyStarted Error = "already started"
|
||||
|
||||
// ErrAlreadyWaiting is the already waiting error.
|
||||
ErrAlreadyWaiting Error = "already waiting"
|
||||
|
||||
// ErrInvalidURLs is the invalid url-opts error.
|
||||
ErrInvalidURLOpts Error = "invalid url-opts"
|
||||
|
||||
// ErrInvalidCmdOpts is the invalid cmd-opts error.
|
||||
ErrInvalidCmdOpts Error = "invalid cmd-opts"
|
||||
|
||||
// ErrInvalidProcessOpts is the invalid process-opts error.
|
||||
ErrInvalidProcessOpts Error = "invalid process-opts"
|
||||
|
||||
// ErrInvalidExecPath is the invalid exec-path error.
|
||||
ErrInvalidExecPath Error = "invalid exec-path"
|
||||
)
|
||||
|
||||
// Runner holds information about a running Chrome process.
|
||||
type Runner struct {
|
||||
opts map[string]interface{}
|
||||
cmd *exec.Cmd
|
||||
waiting bool
|
||||
rw sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates a new Chrome process using the supplied command line options.
|
||||
func New(opts ...CommandLineOption) (*Runner, error) {
|
||||
var err error
|
||||
|
||||
cliOpts := make(map[string]interface{})
|
||||
|
||||
// apply opts
|
||||
for _, o := range opts {
|
||||
if err = o(cliOpts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// set default Chrome options if exec-path not provided
|
||||
if _, ok := cliOpts["exec-path"]; !ok {
|
||||
cliOpts["exec-path"] = LookChromeNames()
|
||||
for k, v := range map[string]interface{}{
|
||||
"no-first-run": true,
|
||||
"no-default-browser-check": true,
|
||||
"remote-debugging-port": 9222,
|
||||
} {
|
||||
if _, ok := cliOpts[k]; !ok {
|
||||
cliOpts[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add KillProcessGroup and ForceKill if no other cmd opts provided
|
||||
if _, ok := cliOpts["cmd-opts"]; !ok {
|
||||
for _, o := range []CommandLineOption{KillProcessGroup, ForceKill} {
|
||||
if err = o(cliOpts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Runner{
|
||||
opts: cliOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// cliOptRE is a regular expression to validate a chrome cli option.
|
||||
var cliOptRE = regexp.MustCompile(`^[a-z0-9\-]+$`)
|
||||
|
||||
// buildOpts generates the command line options for Chrome.
|
||||
func (r *Runner) buildOpts() []string {
|
||||
var opts []string
|
||||
var urls []string
|
||||
|
||||
// process opts
|
||||
for k, v := range r.opts {
|
||||
if !cliOptRE.MatchString(k) || v == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch k {
|
||||
case "exec-path", "cmd-opts", "process-opts":
|
||||
continue
|
||||
|
||||
case "url-opts":
|
||||
urls = v.([]string)
|
||||
|
||||
default:
|
||||
switch z := v.(type) {
|
||||
case bool:
|
||||
if z {
|
||||
opts = append(opts, "--"+k)
|
||||
}
|
||||
|
||||
case string:
|
||||
opts = append(opts, "--"+k+"="+z)
|
||||
|
||||
default:
|
||||
opts = append(opts, "--"+k+"="+fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if urls == nil {
|
||||
urls = append(urls, "about:blank")
|
||||
}
|
||||
|
||||
return append(opts, urls...)
|
||||
}
|
||||
|
||||
// Start starts a Chrome process using the specified context. The Chrome
|
||||
// process can be terminated by closing the passed context.
|
||||
func (r *Runner) Start(ctxt context.Context, opts ...string) error {
|
||||
var err error
|
||||
var ok bool
|
||||
|
||||
r.rw.RLock()
|
||||
cmd := r.cmd
|
||||
r.rw.RUnlock()
|
||||
|
||||
if cmd != nil {
|
||||
return ErrAlreadyStarted
|
||||
}
|
||||
|
||||
// set user data dir, if not provided
|
||||
_, ok = r.opts["user-data-dir"]
|
||||
if !ok {
|
||||
r.opts["user-data-dir"], err = ioutil.TempDir(
|
||||
defaultUserDataTmpDir, fmt.Sprintf(DefaultUserDataDirPrefix, r.Port()),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// get exec path
|
||||
var execPath string
|
||||
if p, ok := r.opts["exec-path"]; ok {
|
||||
execPath, ok = p.(string)
|
||||
if !ok {
|
||||
return ErrInvalidExecPath
|
||||
}
|
||||
}
|
||||
|
||||
// ensure execPath is valid
|
||||
if execPath == "" {
|
||||
return ErrInvalidExecPath
|
||||
}
|
||||
|
||||
// create cmd
|
||||
r.cmd = exec.CommandContext(ctxt, execPath, append(r.buildOpts(), opts...)...)
|
||||
|
||||
// apply cmd opts
|
||||
if cmdOpts, ok := r.opts["cmd-opts"]; ok {
|
||||
for _, co := range cmdOpts.([]func(*exec.Cmd) error) {
|
||||
if err = co(r.cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// start process
|
||||
if err = r.cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply process opts
|
||||
if processOpts, ok := r.opts["process-opts"]; ok {
|
||||
for _, po := range processOpts.([]func(*os.Process) error) {
|
||||
if err = po(r.cmd.Process); err != nil {
|
||||
// TODO: do something better here, as we want to kill
|
||||
// the child process, do cleanup, etc.
|
||||
panic(err)
|
||||
//return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown shuts down the Chrome process.
|
||||
func (r *Runner) Shutdown(ctxt context.Context, opts ...client.Option) error {
|
||||
var err error
|
||||
|
||||
cl := r.Client(opts...)
|
||||
|
||||
targets, err := cl.ListPageTargets(ctxt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, len(targets))
|
||||
for i, t := range targets {
|
||||
wg.Add(1)
|
||||
go func(wg *sync.WaitGroup, i int, t client.Target) {
|
||||
defer wg.Done()
|
||||
errs[i] = cl.CloseTarget(ctxt, t)
|
||||
}(&wg, i, t)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for _, e := range errs {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
// osx applications do not automatically exit when all windows (ie, tabs)
|
||||
// closed, so send SIGTERM.
|
||||
//
|
||||
// TODO: add other behavior here for more process options on shutdown?
|
||||
if runtime.GOOS == "darwin" && r.cmd != nil && r.cmd.Process != nil {
|
||||
return r.cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait waits for the previously started Chrome process to terminate, returning
|
||||
// any encountered error.
|
||||
func (r *Runner) Wait() error {
|
||||
r.rw.RLock()
|
||||
waiting := r.waiting
|
||||
r.rw.RUnlock()
|
||||
|
||||
if waiting {
|
||||
return ErrAlreadyWaiting
|
||||
}
|
||||
|
||||
r.rw.Lock()
|
||||
r.waiting = true
|
||||
r.rw.Unlock()
|
||||
|
||||
defer func() {
|
||||
r.rw.Lock()
|
||||
r.waiting = false
|
||||
r.rw.Unlock()
|
||||
}()
|
||||
|
||||
return r.cmd.Wait()
|
||||
}
|
||||
|
||||
// Port returns the port the process was launched with.
|
||||
func (r *Runner) Port() int {
|
||||
var port interface{}
|
||||
var ok bool
|
||||
port, ok = r.opts["remote-debugging-port"]
|
||||
if !ok {
|
||||
port, ok = r.opts["port"]
|
||||
}
|
||||
if !ok {
|
||||
panic("expected either remote-debugging-port or port to be specified in command line options")
|
||||
}
|
||||
|
||||
var p int
|
||||
p, ok = port.(int)
|
||||
if !ok {
|
||||
panic("expected port to be type int")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Client returns a Chrome DevTools Protocol client for the running Chrome
|
||||
// process.
|
||||
func (r *Runner) Client(opts ...client.Option) *client.Client {
|
||||
return client.New(append(opts,
|
||||
client.URL(fmt.Sprintf("http://localhost:%d/json", r.Port())),
|
||||
)...)
|
||||
}
|
||||
|
||||
// Run starts a new Chrome process runner, using the provided context and
|
||||
// command line options.
|
||||
func Run(ctxt context.Context, opts ...CommandLineOption) (*Runner, error) {
|
||||
var err error
|
||||
|
||||
// create
|
||||
r, err := New(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// start
|
||||
if err = r.Start(ctxt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// CommandLineOption is a runner command line option.
|
||||
//
|
||||
// see: http://peter.sh/experiments/chromium-command-line-switches/
|
||||
type CommandLineOption func(map[string]interface{}) error
|
||||
|
||||
// Flag is a generic command line option to pass a name=value flag to
|
||||
// Chrome.
|
||||
func Flag(name string, value interface{}) CommandLineOption {
|
||||
return func(m map[string]interface{}) error {
|
||||
m[name] = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Path sets the path to the Chrome executable and sets default run options for
|
||||
// Chrome. This will also set the remote debugging port to 9222, and disable
|
||||
// the first run / default browser check.
|
||||
//
|
||||
// Note: use ExecPath if you do not want to set other options.
|
||||
func Path(path string) CommandLineOption {
|
||||
return func(m map[string]interface{}) error {
|
||||
m["exec-path"] = path
|
||||
m["no-first-run"] = true
|
||||
m["no-default-browser-check"] = true
|
||||
m["remote-debugging-port"] = 9222
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ExecPath is a command line option to set the exec path.
|
||||
func ExecPath(path string) CommandLineOption {
|
||||
return Flag("exec-path", path)
|
||||
}
|
||||
|
||||
// UserDataDir is the command line option to set the user data dir.
|
||||
//
|
||||
// Note: set this option to manually set the profile directory used by Chrome.
|
||||
// When this is not set, then a default path will be created in the /tmp
|
||||
// directory.
|
||||
func UserDataDir(dir string) CommandLineOption {
|
||||
return Flag("user-data-dir", dir)
|
||||
}
|
||||
|
||||
// ProxyServer is the command line option to set the outbound proxy server.
|
||||
func ProxyServer(proxy string) CommandLineOption {
|
||||
return Flag("proxy-server", proxy)
|
||||
}
|
||||
|
||||
// WindowSize is the command line option to set the initial window size.
|
||||
func WindowSize(width, height int) CommandLineOption {
|
||||
return Flag("window-size", fmt.Sprintf("%d,%d", width, height))
|
||||
}
|
||||
|
||||
// UserAgent is the command line option to set the default User-Agent
|
||||
// header.
|
||||
func UserAgent(userAgent string) CommandLineOption {
|
||||
return Flag("user-agent", userAgent)
|
||||
}
|
||||
|
||||
// NoSandbox is the Chrome comamnd line option to disable the sandbox.
|
||||
func NoSandbox(m map[string]interface{}) error {
|
||||
return Flag("no-sandbox", true)(m)
|
||||
}
|
||||
|
||||
// NoFirstRun is the Chrome comamnd line option to disable the first run
|
||||
// dialog.
|
||||
func NoFirstRun(m map[string]interface{}) error {
|
||||
return Flag("no-first-run", true)(m)
|
||||
}
|
||||
|
||||
// NoDefaultBrowserCheck is the Chrome comamnd line option to disable the
|
||||
// default browser check.
|
||||
func NoDefaultBrowserCheck(m map[string]interface{}) error {
|
||||
return Flag("no-default-browser-check", true)(m)
|
||||
}
|
||||
|
||||
// RemoteDebuggingPort is the command line option to set the remote
|
||||
// debugging port.
|
||||
func RemoteDebuggingPort(port int) CommandLineOption {
|
||||
return Flag("remote-debugging-port", port)
|
||||
}
|
||||
|
||||
// Headless is the command line option to run in headless mode.
|
||||
func Headless(m map[string]interface{}) error {
|
||||
return Flag("headless", true)(m)
|
||||
}
|
||||
|
||||
// DisableGPU is the command line option to disable the GPU process.
|
||||
func DisableGPU(m map[string]interface{}) error {
|
||||
return Flag("disable-gpu", true)(m)
|
||||
}
|
||||
|
||||
// URL is the command line option to add a URL to open on process start.
|
||||
//
|
||||
// Note: this can be specified multiple times, and each URL will be opened in a
|
||||
// new tab.
|
||||
func URL(urlstr string) CommandLineOption {
|
||||
return func(m map[string]interface{}) error {
|
||||
var urls []string
|
||||
if u, ok := m["url-opts"]; ok {
|
||||
urls, ok = u.([]string)
|
||||
if !ok {
|
||||
return ErrInvalidURLOpts
|
||||
}
|
||||
}
|
||||
m["url-opts"] = append(urls, urlstr)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// CmdOpt is a command line option to modify the underlying exec.Cmd
|
||||
// prior to the call to exec.Cmd.Start in Run.
|
||||
func CmdOpt(o func(*exec.Cmd) error) CommandLineOption {
|
||||
return func(m map[string]interface{}) error {
|
||||
var opts []func(*exec.Cmd) error
|
||||
if e, ok := m["cmd-opts"]; ok {
|
||||
opts, ok = e.([]func(*exec.Cmd) error)
|
||||
if !ok {
|
||||
return ErrInvalidCmdOpts
|
||||
}
|
||||
}
|
||||
m["cmd-opts"] = append(opts, o)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessOpt is a command line option to modify the child os.Process
|
||||
// after the call to exec.Cmd.Start in Run.
|
||||
func ProcessOpt(o func(*os.Process) error) CommandLineOption {
|
||||
return func(m map[string]interface{}) error {
|
||||
var opts []func(*os.Process) error
|
||||
if e, ok := m["process-opts"]; ok {
|
||||
opts, ok = e.([]func(*os.Process) error)
|
||||
if !ok {
|
||||
return ErrInvalidProcessOpts
|
||||
}
|
||||
}
|
||||
m["process-opts"] = append(opts, o)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// LookChromeNames looks for the platform's DefaultChromeNames and any
|
||||
// additional names using exec.LookPath, returning the first encountered
|
||||
// location or the platform's DefaultChromePath if no names are found on the
|
||||
// path.
|
||||
func LookChromeNames(additional ...string) string {
|
||||
for _, p := range append(additional, DefaultChromeNames...) {
|
||||
path, err := exec.LookPath(p)
|
||||
if err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultChromePath
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
// +build darwin freebsd netbsd openbsd
|
||||
|
||||
package runner
|
||||
|
||||
// ForceKill is a Chrome command line option that forces Chrome to be killed
|
||||
// when the parent is killed.
|
||||
//
|
||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true (only for Linux)
|
||||
func ForceKill(m map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
// +build linux
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// ByteCount is a type byte count const.
|
||||
type ByteCount uint64
|
||||
|
||||
// ByteCount values.
|
||||
const (
|
||||
Byte ByteCount = 1
|
||||
Kilobyte ByteCount = 1024 * Byte
|
||||
Megabyte ByteCount = 1024 * Kilobyte
|
||||
Gigabyte ByteCount = 1024 * Megabyte
|
||||
)
|
||||
|
||||
// prlimit invokes the system's prlimit call. Copied from Go source tree.
|
||||
//
|
||||
// Note: this needs either the CAP_SYS_RESOURCE capability, or the invoking
|
||||
// process needs to have the same functional user and group as the pid being
|
||||
// modified.
|
||||
//
|
||||
// see: man 2 prlimit
|
||||
func prlimit(pid int, res int, newv, old *syscall.Rlimit) error {
|
||||
_, _, err := syscall.RawSyscall6(syscall.SYS_PRLIMIT64, uintptr(pid), uintptr(res), uintptr(unsafe.Pointer(newv)), uintptr(unsafe.Pointer(old)), 0, 0)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rlimit is a Chrome command line option to set the soft rlimit value for res
|
||||
// on a running Chrome process.
|
||||
//
|
||||
// Note: uses Linux prlimit system call, and is invoked after the child process
|
||||
// has been started.
|
||||
//
|
||||
// see: man 2 prlimit
|
||||
func Rlimit(res int, cur, max uint64) CommandLineOption {
|
||||
return ProcessOpt(func(p *os.Process) error {
|
||||
return prlimit(p.Pid, syscall.RLIMIT_AS, &syscall.Rlimit{
|
||||
Cur: cur,
|
||||
Max: max,
|
||||
}, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// LimitMemory is a Chrome command line option to set the soft memory limit for
|
||||
// a running Chrome process.
|
||||
//
|
||||
// Note: uses Linux prlimit system call, and is invoked after the child
|
||||
// process has been started.
|
||||
func LimitMemory(mem ByteCount) CommandLineOption {
|
||||
return Rlimit(syscall.RLIMIT_AS, uint64(mem), uint64(mem))
|
||||
}
|
||||
|
||||
// LimitCoreDump is a Chrome command line option to set the soft core dump
|
||||
// limit for a running Chrome process.
|
||||
//
|
||||
// Note: uses Linux prlimit system call, and is invoked after the child
|
||||
// process has been started.
|
||||
func LimitCoreDump(sz ByteCount) CommandLineOption {
|
||||
return Rlimit(syscall.RLIMIT_CORE, uint64(sz), uint64(sz))
|
||||
}
|
||||
|
||||
// ForceKill is a Chrome command line option that forces Chrome to be killed
|
||||
// when the parent is killed.
|
||||
//
|
||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true (only for Linux)
|
||||
func ForceKill(m map[string]interface{}) error {
|
||||
return CmdOpt(func(c *exec.Cmd) error {
|
||||
if c.SysProcAttr == nil {
|
||||
c.SysProcAttr = new(syscall.SysProcAttr)
|
||||
}
|
||||
|
||||
c.SysProcAttr.Pdeathsig = syscall.SIGKILL
|
||||
|
||||
return nil
|
||||
})(m)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
// +build linux darwin freebsd netbsd openbsd
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultUserDataTmpDir is the default directory path for created user
|
||||
// data directories.
|
||||
defaultUserDataTmpDir = "/tmp"
|
||||
)
|
||||
|
||||
// KillProcessGroup is a Chrome command line option that will instruct the
|
||||
// invoked child Chrome process to terminate when the parent process (ie, the
|
||||
// Go application) dies.
|
||||
//
|
||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true and does nothing on Windows.
|
||||
func KillProcessGroup(m map[string]interface{}) error {
|
||||
return CmdOpt(func(c *exec.Cmd) error {
|
||||
if c.SysProcAttr == nil {
|
||||
c.SysProcAttr = new(syscall.SysProcAttr)
|
||||
}
|
||||
|
||||
c.SysProcAttr.Setpgid = true
|
||||
|
||||
return nil
|
||||
})(m)
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package runner
|
||||
|
||||
import "os"
|
||||
|
||||
var (
|
||||
defaultUserDataTmpDir = os.Getenv("USERPROFILE") + `\AppData\Local`
|
||||
)
|
||||
|
||||
// KillProcessGroup is a Chrome command line option that will instruct the
|
||||
// invoked child Chrome process to terminate when the parent process (ie, the
|
||||
// Go application) dies.
|
||||
//
|
||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true and does nothing on Windows.
|
||||
func KillProcessGroup(m map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForceKill is a Chrome command line option that forces Chrome to be killed
|
||||
// when the parent is killed.
|
||||
//
|
||||
// Note: sets exec.Cmd.SysProcAttr.Setpgid = true (only for Linux)
|
||||
func ForceKill(m map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
178
sel.go
178
sel.go
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/dom"
|
||||
@ -26,9 +25,9 @@ tagname
|
||||
type Selector struct {
|
||||
sel interface{}
|
||||
exp int
|
||||
by func(context.Context, *TargetHandler, *cdp.Node) ([]cdp.NodeID, error)
|
||||
wait func(context.Context, *TargetHandler, *cdp.Node, ...cdp.NodeID) ([]*cdp.Node, error)
|
||||
after func(context.Context, *TargetHandler, ...*cdp.Node) error
|
||||
by func(context.Context, *Target, *cdp.Node) ([]cdp.NodeID, error)
|
||||
wait func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error)
|
||||
after func(context.Context, *Target, ...*cdp.Node) error
|
||||
}
|
||||
|
||||
// Query is an action to query for document nodes match the specified sel and
|
||||
@ -56,21 +55,17 @@ func Query(sel interface{}, opts ...QueryOption) Action {
|
||||
}
|
||||
|
||||
// Do satisfies the Action interface.
|
||||
func (s *Selector) Do(ctxt context.Context, h cdp.Executor) error {
|
||||
th, ok := h.(*TargetHandler)
|
||||
func (s *Selector) Do(ctx context.Context, h cdp.Executor) error {
|
||||
th, ok := h.(*Target)
|
||||
if !ok {
|
||||
return ErrInvalidHandler
|
||||
}
|
||||
|
||||
// TODO: fix this
|
||||
ctxt, cancel := context.WithTimeout(ctxt, 100*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
select {
|
||||
case err = <-s.run(ctxt, th):
|
||||
case <-ctxt.Done():
|
||||
err = ctxt.Err()
|
||||
case err = <-s.run(ctx, th):
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
}
|
||||
|
||||
return err
|
||||
@ -79,54 +74,35 @@ func (s *Selector) Do(ctxt context.Context, h cdp.Executor) error {
|
||||
// run runs the selector action, starting over if the original returned nodes
|
||||
// are invalidated prior to finishing the selector's by, wait, check, and after
|
||||
// funcs.
|
||||
func (s *Selector) run(ctxt context.Context, h *TargetHandler) chan error {
|
||||
func (s *Selector) run(ctx context.Context, h *Target) chan error {
|
||||
ch := make(chan error, 1)
|
||||
h.waitQueue <- func(cur *cdp.Frame) bool {
|
||||
cur.RLock()
|
||||
root := cur.Root
|
||||
cur.RUnlock()
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
if root == nil {
|
||||
// not ready?
|
||||
return false
|
||||
}
|
||||
|
||||
for {
|
||||
root, err := h.GetRoot(ctxt)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctxt.Done():
|
||||
ch <- ctxt.Err()
|
||||
return
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
ids, err := s.by(ctx, h, root)
|
||||
if err != nil || len(ids) < s.exp {
|
||||
return false
|
||||
}
|
||||
nodes, err := s.wait(ctx, h, cur, ids...)
|
||||
// if nodes==nil, we're not yet ready
|
||||
if nodes == nil || err != nil {
|
||||
return false
|
||||
}
|
||||
if s.after != nil {
|
||||
if err := s.after(ctx, h, nodes...); err != nil {
|
||||
ch <- err
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
close(ch)
|
||||
return true
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
@ -139,20 +115,10 @@ func (s *Selector) selAsString() string {
|
||||
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
|
||||
// query options, and after the visibility conditions of the query have been
|
||||
// met, will execute f.
|
||||
func QueryAfter(sel interface{}, f func(context.Context, *TargetHandler, ...*cdp.Node) error, opts ...QueryOption) Action {
|
||||
func QueryAfter(sel interface{}, f func(context.Context, *Target, ...*cdp.Node) error, opts ...QueryOption) Action {
|
||||
return Query(sel, append(opts, After(f))...)
|
||||
}
|
||||
|
||||
@ -160,7 +126,7 @@ func QueryAfter(sel interface{}, f func(context.Context, *TargetHandler, ...*cdp
|
||||
type QueryOption func(*Selector)
|
||||
|
||||
// ByFunc is a query option to set the func used to select elements.
|
||||
func ByFunc(f func(context.Context, *TargetHandler, *cdp.Node) ([]cdp.NodeID, error)) QueryOption {
|
||||
func ByFunc(f func(context.Context, *Target, *cdp.Node) ([]cdp.NodeID, error)) QueryOption {
|
||||
return func(s *Selector) {
|
||||
s.by = f
|
||||
}
|
||||
@ -169,8 +135,8 @@ func ByFunc(f func(context.Context, *TargetHandler, *cdp.Node) ([]cdp.NodeID, er
|
||||
// ByQuery is a query option to select a single element using
|
||||
// DOM.querySelector.
|
||||
func ByQuery(s *Selector) {
|
||||
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||
nodeID, err := dom.QuerySelector(n.NodeID, s.selAsString()).Do(ctxt, h)
|
||||
ByFunc(func(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||
nodeID, err := dom.QuerySelector(n.NodeID, s.selAsString()).Do(ctx, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -185,8 +151,8 @@ func ByQuery(s *Selector) {
|
||||
|
||||
// ByQueryAll is a query option to select elements by DOM.querySelectorAll.
|
||||
func ByQueryAll(s *Selector) {
|
||||
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||
return dom.QuerySelectorAll(n.NodeID, s.selAsString()).Do(ctxt, h)
|
||||
ByFunc(func(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||
return dom.QuerySelectorAll(n.NodeID, s.selAsString()).Do(ctx, h)
|
||||
})(s)
|
||||
}
|
||||
|
||||
@ -199,8 +165,8 @@ func ByID(s *Selector) {
|
||||
// BySearch is a query option via DOM.performSearch (works with both CSS and
|
||||
// XPath queries).
|
||||
func BySearch(s *Selector) {
|
||||
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||
id, count, err := dom.PerformSearch(s.selAsString()).Do(ctxt, h)
|
||||
ByFunc(func(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||
id, count, err := dom.PerformSearch(s.selAsString()).Do(ctx, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -209,7 +175,7 @@ func BySearch(s *Selector) {
|
||||
return []cdp.NodeID{}, nil
|
||||
}
|
||||
|
||||
nodes, err := dom.GetSearchResults(id, 0, count).Do(ctxt, h)
|
||||
nodes, err := dom.GetSearchResults(id, 0, count).Do(ctx, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -225,9 +191,9 @@ func ByNodeID(s *Selector) {
|
||||
panic("ByNodeID can only work on []cdp.NodeID")
|
||||
}
|
||||
|
||||
ByFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||
ByFunc(func(ctx context.Context, h *Target, n *cdp.Node) ([]cdp.NodeID, error) {
|
||||
for _, id := range ids {
|
||||
err := dom.RequestChildNodes(id).WithPierce(true).Do(ctxt, h)
|
||||
err := dom.RequestChildNodes(id).WithPierce(true).Do(ctx, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -238,38 +204,28 @@ func ByNodeID(s *Selector) {
|
||||
}
|
||||
|
||||
// waitReady waits for the specified nodes to be ready.
|
||||
func (s *Selector) waitReady(check func(context.Context, *TargetHandler, *cdp.Node) error) func(context.Context, *TargetHandler, *cdp.Node, ...cdp.NodeID) ([]*cdp.Node, error) {
|
||||
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)
|
||||
func (s *Selector) waitReady(check func(context.Context, *Target, *cdp.Node) error) func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error) {
|
||||
return func(ctx context.Context, h *Target, cur *cdp.Frame, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
||||
nodes := make([]*cdp.Node, len(ids))
|
||||
errs := make([]error, len(ids))
|
||||
cur.RLock()
|
||||
for i, id := range ids {
|
||||
wg.Add(1)
|
||||
go func(i int, id cdp.NodeID) {
|
||||
defer wg.Done()
|
||||
nodes[i], errs[i] = h.WaitNode(ctxt, f, id)
|
||||
}(i, id)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
nodes[i] = cur.Nodes[id]
|
||||
if nodes[i] == nil {
|
||||
cur.RUnlock()
|
||||
// not yet ready
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
cur.RUnlock()
|
||||
|
||||
if check != nil {
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, len(nodes))
|
||||
for i, n := range nodes {
|
||||
wg.Add(1)
|
||||
go func(i int, n *cdp.Node) {
|
||||
defer wg.Done()
|
||||
errs[i] = check(ctxt, h, n)
|
||||
errs[i] = check(ctx, h, n)
|
||||
}(i, n)
|
||||
}
|
||||
wg.Wait()
|
||||
@ -286,7 +242,7 @@ func (s *Selector) waitReady(check func(context.Context, *TargetHandler, *cdp.No
|
||||
}
|
||||
|
||||
// WaitFunc is a query option to set a custom wait func.
|
||||
func WaitFunc(wait func(context.Context, *TargetHandler, *cdp.Node, ...cdp.NodeID) ([]*cdp.Node, error)) QueryOption {
|
||||
func WaitFunc(wait func(context.Context, *Target, *cdp.Frame, ...cdp.NodeID) ([]*cdp.Node, error)) QueryOption {
|
||||
return func(s *Selector) {
|
||||
s.wait = wait
|
||||
}
|
||||
@ -299,9 +255,9 @@ func NodeReady(s *Selector) {
|
||||
|
||||
// NodeVisible is a query option to wait until the element is visible.
|
||||
func NodeVisible(s *Selector) {
|
||||
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
||||
WaitFunc(s.waitReady(func(ctx context.Context, h *Target, n *cdp.Node) error {
|
||||
// check box model
|
||||
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
||||
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx, h)
|
||||
if err != nil {
|
||||
if isCouldNotComputeBoxModelError(err) {
|
||||
return ErrNotVisible
|
||||
@ -312,7 +268,7 @@ func NodeVisible(s *Selector) {
|
||||
|
||||
// check offsetParent
|
||||
var res bool
|
||||
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h)
|
||||
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -325,9 +281,9 @@ func NodeVisible(s *Selector) {
|
||||
|
||||
// NodeNotVisible is a query option to wait until the element is not visible.
|
||||
func NodeNotVisible(s *Selector) {
|
||||
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
||||
WaitFunc(s.waitReady(func(ctx context.Context, h *Target, n *cdp.Node) error {
|
||||
// check box model
|
||||
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctxt, h)
|
||||
_, err := dom.GetBoxModel().WithNodeID(n.NodeID).Do(ctx, h)
|
||||
if err != nil {
|
||||
if isCouldNotComputeBoxModelError(err) {
|
||||
return nil
|
||||
@ -338,7 +294,7 @@ func NodeNotVisible(s *Selector) {
|
||||
|
||||
// check offsetParent
|
||||
var res bool
|
||||
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctxt, h)
|
||||
err = EvaluateAsDevTools(fmt.Sprintf(visibleJS, n.FullXPath()), &res).Do(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -351,7 +307,7 @@ func NodeNotVisible(s *Selector) {
|
||||
|
||||
// NodeEnabled is a query option to wait until the element is enabled.
|
||||
func NodeEnabled(s *Selector) {
|
||||
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
||||
WaitFunc(s.waitReady(func(ctx context.Context, h *Target, n *cdp.Node) error {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
|
||||
@ -367,7 +323,7 @@ func NodeEnabled(s *Selector) {
|
||||
|
||||
// NodeSelected is a query option to wait until the element is selected.
|
||||
func NodeSelected(s *Selector) {
|
||||
WaitFunc(s.waitReady(func(ctxt context.Context, h *TargetHandler, n *cdp.Node) error {
|
||||
WaitFunc(s.waitReady(func(ctx context.Context, h *Target, n *cdp.Node) error {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
|
||||
@ -381,11 +337,11 @@ func NodeSelected(s *Selector) {
|
||||
}))(s)
|
||||
}
|
||||
|
||||
// NodeNotPresent is a query option to wait until no elements match are
|
||||
// present matching the selector.
|
||||
// NodeNotPresent is a query option to wait until no elements are present
|
||||
// matching the selector.
|
||||
func NodeNotPresent(s *Selector) {
|
||||
s.exp = 0
|
||||
WaitFunc(func(ctxt context.Context, h *TargetHandler, n *cdp.Node, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
||||
WaitFunc(func(ctx context.Context, h *Target, cur *cdp.Frame, ids ...cdp.NodeID) ([]*cdp.Node, error) {
|
||||
if len(ids) != 0 {
|
||||
return nil, ErrHasResults
|
||||
}
|
||||
@ -403,7 +359,7 @@ func AtLeast(n int) QueryOption {
|
||||
|
||||
// After is a query option to set a func that will be executed after the wait
|
||||
// has succeeded.
|
||||
func After(f func(context.Context, *TargetHandler, ...*cdp.Node) error) QueryOption {
|
||||
func After(f func(context.Context, *Target, ...*cdp.Node) error) QueryOption {
|
||||
return func(s *Selector) {
|
||||
s.after = f
|
||||
}
|
||||
|
163
sel_test.go
163
sel_test.go
@ -9,26 +9,21 @@ import (
|
||||
func TestWaitReady(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
var nodeIDs []cdp.NodeID
|
||||
err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if len(nodeIDs) != 1 {
|
||||
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
|
||||
err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
WaitReady("#input2", ByID),
|
||||
Value(nodeIDs, &value, ByNodeID),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
}
|
||||
@ -36,26 +31,21 @@ func TestWaitReady(t *testing.T) {
|
||||
func TestWaitVisible(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
var nodeIDs []cdp.NodeID
|
||||
err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if len(nodeIDs) != 1 {
|
||||
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
|
||||
err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
WaitVisible("#input2", ByID),
|
||||
Value(nodeIDs, &value, ByNodeID),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
}
|
||||
@ -63,31 +53,22 @@ func TestWaitVisible(t *testing.T) {
|
||||
func TestWaitNotVisible(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
var nodeIDs []cdp.NodeID
|
||||
err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if len(nodeIDs) != 1 {
|
||||
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
|
||||
err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
Click("#button2", ByID),
|
||||
WaitNotVisible("#input2", ByID),
|
||||
Value(nodeIDs, &value, ByNodeID),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
}
|
||||
@ -95,46 +76,35 @@ func TestWaitNotVisible(t *testing.T) {
|
||||
func TestWaitEnabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
var attr string
|
||||
var ok bool
|
||||
err := c.Run(defaultContext, AttributeValue("#select1", "disabled", &attr, &ok, ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx, AttributeValue("#select1", "disabled", &attr, &ok, ByID)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("expected element to be disabled")
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, Click("#button3", ByID))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := Run(ctx,
|
||||
Click("#button3", ByID),
|
||||
WaitEnabled("#select1", ByID),
|
||||
AttributeValue("#select1", "disabled", &attr, &ok, ByID),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("expected element to be enabled")
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
var value string
|
||||
err = c.Run(defaultContext, Value("#select1", &value, ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"),
|
||||
Value("#select1", &value, ByID),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
if value != "foo" {
|
||||
t.Fatalf("expected value to be foo, got: %s", value)
|
||||
}
|
||||
@ -143,43 +113,32 @@ func TestWaitEnabled(t *testing.T) {
|
||||
func TestWaitSelected(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
err := c.Run(defaultContext, Click("#button3", ByID))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, WaitEnabled("#select1", ByID))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
Click("#button3", ByID),
|
||||
WaitEnabled("#select1", ByID),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
var attr string
|
||||
ok := false
|
||||
err = c.Run(defaultContext, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, &ok))
|
||||
if err != nil {
|
||||
if err := Run(ctx, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, &ok)); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("expected element to be not selected")
|
||||
}
|
||||
|
||||
err = c.Run(defaultContext, SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"))
|
||||
if err != nil {
|
||||
if err := Run(ctx,
|
||||
SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true"),
|
||||
WaitSelected(`//*[@id="select1"]/option[1]`),
|
||||
AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, nil),
|
||||
); err != nil {
|
||||
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" {
|
||||
t.Fatal("expected element to be selected")
|
||||
}
|
||||
@ -188,21 +147,14 @@ func TestWaitSelected(t *testing.T) {
|
||||
func TestWaitNotPresent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
err := c.Run(defaultContext, WaitVisible("#input3", ByID))
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := Run(ctx,
|
||||
WaitVisible("#input3", ByID),
|
||||
Click("#button4", ByID),
|
||||
WaitNotPresent("#input3", ByID),
|
||||
); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
}
|
||||
@ -210,12 +162,11 @@ func TestWaitNotPresent(t *testing.T) {
|
||||
func TestAtLeast(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := testAllocate(t, "js.html")
|
||||
defer c.Release()
|
||||
ctx, cancel := testAllocate(t, "js.html")
|
||||
defer cancel()
|
||||
|
||||
var nodes []*cdp.Node
|
||||
err := c.Run(defaultContext, Nodes("//input", &nodes, AtLeast(3)))
|
||||
if err != nil {
|
||||
if err := Run(ctx, Nodes("//input", &nodes, AtLeast(3))); err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if len(nodes) < 3 {
|
||||
|
320
target.go
Normal file
320
target.go
Normal file
@ -0,0 +1,320 @@
|
||||
package chromedp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
|
||||
"github.com/chromedp/cdproto"
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/dom"
|
||||
"github.com/chromedp/cdproto/inspector"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/cdproto/target"
|
||||
)
|
||||
|
||||
// Target manages a Chrome DevTools Protocol target.
|
||||
type Target struct {
|
||||
browser *Browser
|
||||
SessionID target.SessionID
|
||||
TargetID target.ID
|
||||
|
||||
waitQueue chan func(cur *cdp.Frame) bool
|
||||
eventQueue chan *cdproto.Message
|
||||
|
||||
// below are the old TargetHandler fields.
|
||||
|
||||
// frames is the set of encountered frames.
|
||||
frames map[cdp.FrameID]*cdp.Frame
|
||||
|
||||
// cur is the current top level frame.
|
||||
cur *cdp.Frame
|
||||
|
||||
// logging funcs
|
||||
logf, errf func(string, ...interface{})
|
||||
}
|
||||
|
||||
func (t *Target) run(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case msg := <-t.eventQueue:
|
||||
if err := t.processEvent(ctx, msg); err != nil {
|
||||
t.errf("could not process event: %v", err)
|
||||
continue
|
||||
}
|
||||
default:
|
||||
// prevent busy spinning. TODO: do better
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
n := len(t.waitQueue)
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
if t.cur == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
fn := <-t.waitQueue
|
||||
if !fn(t.cur) {
|
||||
// try again later.
|
||||
t.waitQueue <- fn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Target) Execute(ctx context.Context, method string, params json.Marshaler, res json.Unmarshaler) error {
|
||||
paramsMsg := emptyObj
|
||||
if params != nil {
|
||||
var err error
|
||||
if paramsMsg, err = json.Marshal(params); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
innerID := atomic.AddInt64(&t.browser.next, 1)
|
||||
msg := &cdproto.Message{
|
||||
ID: innerID,
|
||||
Method: cdproto.MethodType(method),
|
||||
Params: paramsMsg,
|
||||
}
|
||||
msgJSON, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sendParams := target.SendMessageToTarget(string(msgJSON)).
|
||||
WithSessionID(t.SessionID)
|
||||
sendParamsJSON, _ := json.Marshal(sendParams)
|
||||
|
||||
// We want to grab the response from the inner message.
|
||||
ch := make(chan *cdproto.Message, 1)
|
||||
t.browser.cmdQueue <- cmdJob{
|
||||
msg: &cdproto.Message{ID: innerID},
|
||||
resp: ch,
|
||||
}
|
||||
|
||||
// The response from the outer message is uninteresting; pass a nil
|
||||
// resp channel.
|
||||
outerID := atomic.AddInt64(&t.browser.next, 1)
|
||||
t.browser.cmdQueue <- cmdJob{
|
||||
msg: &cdproto.Message{
|
||||
ID: outerID,
|
||||
Method: target.CommandSendMessageToTarget,
|
||||
Params: sendParamsJSON,
|
||||
},
|
||||
}
|
||||
|
||||
select {
|
||||
case msg := <-ch:
|
||||
switch {
|
||||
case msg == nil:
|
||||
return ErrChannelClosed
|
||||
case msg.Error != nil:
|
||||
return msg.Error
|
||||
case res != nil:
|
||||
return json.Unmarshal(msg.Result, res)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// below are the old TargetHandler methods.
|
||||
|
||||
// processEvent processes an incoming event.
|
||||
func (t *Target) processEvent(ctx context.Context, msg *cdproto.Message) error {
|
||||
if msg == nil {
|
||||
return ErrChannelClosed
|
||||
}
|
||||
// unmarshal
|
||||
ev, err := cdproto.UnmarshalMessage(msg)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "unknown command or event") {
|
||||
// This is most likely an event received from an older
|
||||
// Chrome which a newer cdproto doesn't have, as it is
|
||||
// deprecated. Ignore that error.
|
||||
// TODO: use error wrapping once Go 1.13 is released.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch ev.(type) {
|
||||
case *inspector.EventDetached:
|
||||
return nil
|
||||
case *dom.EventDocumentUpdated:
|
||||
t.documentUpdated(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg.Method.Domain() {
|
||||
case "Page":
|
||||
t.pageEvent(ev)
|
||||
case "DOM":
|
||||
t.domEvent(ev)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// documentUpdated handles the document updated event, retrieving the document
|
||||
// root for the root frame.
|
||||
func (t *Target) documentUpdated(ctx context.Context) {
|
||||
f := t.cur
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
|
||||
// invalidate nodes
|
||||
if f.Root != nil {
|
||||
close(f.Root.Invalidated)
|
||||
}
|
||||
|
||||
f.Nodes = make(map[cdp.NodeID]*cdp.Node)
|
||||
var err error
|
||||
f.Root, err = dom.GetDocument().WithPierce(true).Do(ctx, t)
|
||||
if err == context.Canceled {
|
||||
return // TODO: perhaps not necessary, but useful to keep the tests less noisy
|
||||
}
|
||||
if err != nil {
|
||||
t.errf("could not retrieve document root for %s: %v", f.ID, err)
|
||||
return
|
||||
}
|
||||
f.Root.Invalidated = make(chan struct{})
|
||||
walk(f.Nodes, f.Root)
|
||||
}
|
||||
|
||||
// emptyObj is an empty JSON object message.
|
||||
var emptyObj = easyjson.RawMessage([]byte(`{}`))
|
||||
|
||||
// pageEvent handles incoming page events.
|
||||
func (t *Target) pageEvent(ev interface{}) {
|
||||
var id cdp.FrameID
|
||||
var op frameOp
|
||||
|
||||
switch e := ev.(type) {
|
||||
case *page.EventFrameNavigated:
|
||||
t.frames[e.Frame.ID] = e.Frame
|
||||
t.cur = e.Frame
|
||||
return
|
||||
|
||||
case *page.EventFrameAttached:
|
||||
id, op = e.FrameID, frameAttached(e.ParentFrameID)
|
||||
|
||||
case *page.EventFrameDetached:
|
||||
id, op = e.FrameID, frameDetached
|
||||
|
||||
case *page.EventFrameStartedLoading:
|
||||
id, op = e.FrameID, frameStartedLoading
|
||||
|
||||
case *page.EventFrameStoppedLoading:
|
||||
id, op = e.FrameID, frameStoppedLoading
|
||||
|
||||
// ignored events
|
||||
case *page.EventFrameRequestedNavigation:
|
||||
return
|
||||
case *page.EventDomContentEventFired:
|
||||
return
|
||||
case *page.EventLoadEventFired:
|
||||
return
|
||||
case *page.EventFrameResized:
|
||||
return
|
||||
case *page.EventLifecycleEvent:
|
||||
return
|
||||
case *page.EventNavigatedWithinDocument:
|
||||
return
|
||||
|
||||
default:
|
||||
t.errf("unhandled page event %T", ev)
|
||||
return
|
||||
}
|
||||
|
||||
f := t.frames[id]
|
||||
if f == nil {
|
||||
// This can happen if a frame is attached or starts loading
|
||||
// before it's ever navigated to. We won't have all the frame
|
||||
// details just yet, but that's okay.
|
||||
f = &cdp.Frame{ID: id}
|
||||
t.frames[id] = f
|
||||
}
|
||||
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
|
||||
op(f)
|
||||
}
|
||||
|
||||
// domEvent handles incoming DOM events.
|
||||
func (t *Target) domEvent(ev interface{}) {
|
||||
f := t.cur
|
||||
|
||||
var id cdp.NodeID
|
||||
var op nodeOp
|
||||
|
||||
switch e := ev.(type) {
|
||||
case *dom.EventSetChildNodes:
|
||||
id, op = e.ParentID, setChildNodes(f.Nodes, e.Nodes)
|
||||
|
||||
case *dom.EventAttributeModified:
|
||||
id, op = e.NodeID, attributeModified(e.Name, e.Value)
|
||||
|
||||
case *dom.EventAttributeRemoved:
|
||||
id, op = e.NodeID, attributeRemoved(e.Name)
|
||||
|
||||
case *dom.EventInlineStyleInvalidated:
|
||||
if len(e.NodeIds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
id, op = e.NodeIds[0], inlineStyleInvalidated(e.NodeIds[1:])
|
||||
|
||||
case *dom.EventCharacterDataModified:
|
||||
id, op = e.NodeID, characterDataModified(e.CharacterData)
|
||||
|
||||
case *dom.EventChildNodeCountUpdated:
|
||||
id, op = e.NodeID, childNodeCountUpdated(e.ChildNodeCount)
|
||||
|
||||
case *dom.EventChildNodeInserted:
|
||||
id, op = e.ParentNodeID, childNodeInserted(f.Nodes, e.PreviousNodeID, e.Node)
|
||||
|
||||
case *dom.EventChildNodeRemoved:
|
||||
id, op = e.ParentNodeID, childNodeRemoved(f.Nodes, e.NodeID)
|
||||
|
||||
case *dom.EventShadowRootPushed:
|
||||
id, op = e.HostID, shadowRootPushed(f.Nodes, e.Root)
|
||||
|
||||
case *dom.EventShadowRootPopped:
|
||||
id, op = e.HostID, shadowRootPopped(f.Nodes, e.RootID)
|
||||
|
||||
case *dom.EventPseudoElementAdded:
|
||||
id, op = e.ParentID, pseudoElementAdded(f.Nodes, e.PseudoElement)
|
||||
|
||||
case *dom.EventPseudoElementRemoved:
|
||||
id, op = e.ParentID, pseudoElementRemoved(f.Nodes, e.PseudoElementID)
|
||||
|
||||
case *dom.EventDistributedNodesUpdated:
|
||||
id, op = e.InsertionPointID, distributedNodesUpdated(e.DistributedNodes)
|
||||
|
||||
default:
|
||||
t.errf("unhandled node event %T", ev)
|
||||
return
|
||||
}
|
||||
|
||||
n, ok := f.Nodes[id]
|
||||
if !ok {
|
||||
// Node ID has been invalidated. Nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
|
||||
op(n)
|
||||
}
|
||||
|
||||
type TargetOption func(*Target)
|
9
testdata/iframe.html
vendored
Normal file
9
testdata/iframe.html
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>page with an iframe</title>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="form.html"></iframe>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user