Full Code of chromedp/chromedp for AI

master 9d449ea08b61 cached
74 files
416.8 KB
127.6k tokens
738 symbols
1 requests
Download .txt
Showing preview only (437K chars total). Download the full file or copy to clipboard to get everything.
Repository: chromedp/chromedp
Branch: master
Commit: 9d449ea08b61
Files: 74
Total size: 416.8 KB

Directory structure:
gitextract_oahvn68_/

├── .github/
│   ├── ISSUE_TEMPLATE
│   └── workflows/
│       └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── allocate.go
├── allocate_linux.go
├── allocate_other.go
├── allocate_test.go
├── browser.go
├── browser_test.go
├── call.go
├── chromedp.go
├── chromedp_test.go
├── conn.go
├── contrib/
│   └── docker-test.sh
├── device/
│   ├── device.go
│   └── gen.go
├── emulate.go
├── emulate_test.go
├── errors.go
├── eval.go
├── eval_test.go
├── event_test.go
├── example_test.go
├── go.mod
├── go.sum
├── input.go
├── input_test.go
├── js/
│   ├── attribute.js
│   ├── blur.js
│   ├── getClientRect.js
│   ├── reset.js
│   ├── setAttribute.js
│   ├── submit.js
│   ├── text.js
│   ├── textContent.js
│   ├── visible.js
│   └── waitForPredicatePageFunction.js
├── js.go
├── kb/
│   ├── gen.go
│   └── kb.go
├── nav.go
├── nav_test.go
├── poll.go
├── poll_test.go
├── query.go
├── query_test.go
├── screenshot.go
├── screenshot_test.go
├── target.go
├── testdata/
│   ├── alert.html
│   ├── child1.html
│   ├── child2.html
│   ├── consolespam.html
│   ├── dialog.html
│   ├── form.html
│   ├── frameset.html
│   ├── grid.html
│   ├── iframe.html
│   ├── image.html
│   ├── image2.html
│   ├── input.html
│   ├── js.html
│   ├── nested.html
│   ├── newtab.html
│   ├── poll.html
│   ├── screenshot.html
│   ├── svg.html
│   ├── table.html
│   ├── visible.html
│   └── webgl.html
├── util.go
└── util_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE
================================================
<!---
This issue tracker is mainly for bugs and feature requests. Before asking a
question, search online and try to investigate on your own.
-->

#### What versions are you running?

```console
$ go list -m github.com/chromedp/chromedp
<!--replace this line with the output-->
$ google-chrome --version
<!--replace this line with the output-->
$ go version
<!--replace this line with the output-->
```

#### What did you do? Include clear steps.

<!--Put your source code in fenced code block like this:-->

```go
put your source code here
```

#### What did you expect to see?


#### What did you see instead?


================================================
FILE: .github/workflows/test.yml
================================================
on: [push, pull_request]
name: Test
jobs:
  test:
    strategy:
      matrix:
        go-version: [oldstable, stable]
    runs-on: ubuntu-latest
    steps:
      - name: Install Go
        uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go-version }}
      - name: Install Packages
        run: |
          sudo apt-get -qq update
          sudo apt-get install -y build-essential
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Test Chrome
        run: go test -v ./...
      - name: Test headless-shell
        run: ./contrib/docker-test.sh


================================================
FILE: .gitignore
================================================
out.txt
out*.txt
old*.txt
cdp-*.log
cdp-*.txt
*.out

/chromedp.test
/chromedp.test.exe

/*.jpeg
/*.png
/*.pdf


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016-2025 Kenneth Shaw

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# About chromedp

Package `chromedp` is a faster, simpler way to drive browsers supporting the
[Chrome DevTools Protocol][devtools-protocol] in Go without external dependencies.

[![Unit Tests][chromedp-ci-status]][chromedp-ci]
[![Go Reference][goref-chromedp-status]][goref-chromedp]
[![Releases][release-status]][releases]

## Installing

Install in the usual Go way:

```sh
$ go get -u github.com/chromedp/chromedp
```

## Examples

Refer to the [Go reference][goref-chromedp] for the documentation and examples.
Additionally, the [examples][chromedp-examples] repository contains more
examples on complex actions, and other common high-level tasks such as taking
full page screenshots.

## Frequently Asked Questions

> I can't see any Chrome browser window

By default, Chrome is run in headless mode. See `DefaultExecAllocatorOptions`, and
[an example][goref-chromedp-exec-allocator] to override the default options.

> I'm seeing "context canceled" errors

When the connection to the browser is lost, `chromedp` cancels the context, and
it may result in this error. This occurs, for example, if the browser is closed
manually, or if the browser process has been killed or otherwise terminated.

> Chrome exits as soon as my Go program finishes

On Linux, `chromedp` is configured to avoid leaking resources by force-killing
any started Chrome child processes. If you need to launch a long-running Chrome
instance, manually start Chrome and connect using `RemoteAllocator`.

> Executing an action without `Run` results in "invalid context"

By default, a `chromedp` context does not have an executor, however one can be
specified manually if necessary; see [issue #326][github-326]
for an example.

> I can't use an `Action` with `Run` because it returns many values

Wrap it with an `ActionFunc`:

```go
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
	_, err := domain.SomeAction().Do(ctx)
	return err
}))
```

> I want to use chromedp on a headless environment

The simplest way is to run the Go program that uses chromedp inside the
[chromedp/headless-shell][docker-headless-shell] image. That image contains
`headless-shell`, a smaller headless build of Chrome, which `chromedp` is able
to find out of the box.

## Resources

* [`headless-shell`][docker-headless-shell] - A build of `headless-shell` that is used for testing `chromedp`
* [chromedp: A New Way to Drive the Web][gophercon-2017-presentation] - GopherCon SG 2017 talk
* [Chrome DevTools Protocol][devtools-protocol] - Chrome DevTools Protocol reference
* [chromedp examples][chromedp-examples] - More complicated examples for `chromedp`
* [`github.com/chromedp/cdproto`][goref-cdproto] - Go reference for the generated Chrome DevTools Protocol API
* [`github.com/chromedp/pdlgen`][chromedp-pdlgen] - tool used to generate `cdproto`
* [`github.com/chromedp/chromedp-proxy`][chromedp-proxy] - a simple CDP proxy for logging CDP clients and browsers

[chromedp-ci]: https://github.com/chromedp/chromedp/actions/workflows/test.yml (Test CI)
[chromedp-ci-status]: https://github.com/chromedp/chromedp/actions/workflows/test.yml/badge.svg (Test CI)
[chromedp-examples]: https://github.com/chromedp/examples
[chromedp-pdlgen]: https://github.com/chromedp/pdlgen
[chromedp-proxy]: https://github.com/chromedp/chromedp-proxy
[devtools-protocol]: https://chromedevtools.github.io/devtools-protocol/
[docker-headless-shell]: https://hub.docker.com/r/chromedp/headless-shell/
[github-326]: https://github.com/chromedp/chromedp/issues/326
[gophercon-2017-presentation]: https://www.youtube.com/watch?v=_7pWCg94sKw
[goref-cdproto]: https://pkg.go.dev/github.com/chromedp/cdproto
[goref-chromedp-exec-allocator]: https://pkg.go.dev/github.com/chromedp/chromedp#example-ExecAllocator
[goref-chromedp]: https://pkg.go.dev/github.com/chromedp/chromedp
[goref-chromedp-status]: https://pkg.go.dev/badge/github.com/chromedp/chromedp.svg
[release-status]: https://img.shields.io/github/v/release/chromedp/chromedp?display_name=tag&sort=semver (Latest Release)
[releases]: https://github.com/chromedp/chromedp/releases (Releases)


================================================
FILE: allocate.go
================================================
package chromedp

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sync"
	"time"
)

// 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]any),
		wsURLReadTimeout: 20 * time.Second,
	}
	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. Do not modify
// this global; instead, use NewExecAllocator. See [ExampleExecAllocator].
//
// [ExampleExecAllocator]: https://pkg.go.dev/github.com/chromedp/chromedp#example-ExecAllocator
var DefaultExecAllocatorOptions = [...]ExecAllocatorOption{
	NoFirstRun,
	NoDefaultBrowserCheck,
	Headless,

	// After Puppeteer's default behavior.
	Flag("disable-background-networking", true),
	Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
	Flag("disable-background-timer-throttling", true),
	Flag("disable-backgrounding-occluded-windows", true),
	Flag("disable-breakpad", true),
	Flag("disable-client-side-phishing-detection", true),
	Flag("disable-default-apps", true),
	Flag("disable-dev-shm-usage", true),
	Flag("disable-extensions", true),
	Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
	Flag("disable-hang-monitor", true),
	Flag("disable-ipc-flooding-protection", true),
	Flag("disable-popup-blocking", true),
	Flag("disable-prompt-on-repost", true),
	Flag("disable-renderer-backgrounding", true),
	Flag("disable-sync", true),
	Flag("force-color-profile", "srgb"),
	Flag("metrics-recording-only", true),
	Flag("safebrowsing-disable-auto-update", true),
	Flag("enable-automation", true),
	Flag("password-store", "basic"),
	Flag("use-mock-keychain", true),
}

// 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 an 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]any
	initEnv   []string

	// Chrome will sometimes fail to print the websocket, or run for a long
	// time, without properly exiting. To avoid blocking forever in those
	// cases, give up after a specified timeout.
	wsURLReadTimeout time.Duration

	modifyCmdFunc func(cmd *exec.Cmd)

	wg sync.WaitGroup

	combinedOutputWriter io.Writer
}

// allocTempDir is used to group all ExecAllocator temporary user data dirs in
// the same location, useful for the tests. If left empty, the system's default
// temporary directory is used.
var allocTempDir string

// Allocate satisfies the Allocator interface.
func (a *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 a.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 := a.initFlags["user-data-dir"].(string)
	if !ok {
		tempDir, err := os.MkdirTemp(allocTempDir, "chromedp-runner")
		if err != nil {
			return nil, err
		}
		args = append(args, "--user-data-dir="+tempDir)
		dataDir = tempDir
		removeDir = true
	}
	if _, ok := a.initFlags["no-sandbox"]; !ok && os.Getuid() == 0 {
		// Running as root, for example in a Linux container. Chrome
		// needs --no-sandbox when running as root, so make that the
		// default, unless the user set Flag("no-sandbox", false).
		args = append(args, "--no-sandbox")
	}
	if _, ok := a.initFlags["remote-debugging-port"]; !ok {
		args = append(args, "--remote-debugging-port=0")
	}

	// Force the first page to be blank, instead of the welcome page;
	// --no-first-run doesn't enforce that.
	args = append(args, "about:blank")

	cmd := exec.CommandContext(ctx, a.execPath, args...)
	defer func() {
		if removeDir && cmd.Process == nil {
			// We couldn't start the process, so we didn't get to
			// the goroutine that handles RemoveAll below. Remove it
			// to not leave an empty directory.
			os.RemoveAll(dataDir)
		}
	}()

	if a.modifyCmdFunc != nil {
		a.modifyCmdFunc(cmd)
	} else {
		allocateCmdOptions(cmd)
	}

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}
	cmd.Stderr = cmd.Stdout

	// Preserve environment variables set in the (lowest priority) existing
	// environment, OverrideCmdFunc(), and Env (highest priority)
	if len(a.initEnv) > 0 || len(cmd.Env) > 0 {
		cmd.Env = append(os.Environ(), cmd.Env...)
		cmd.Env = append(cmd.Env, a.initEnv...)
	}

	// We must start the cmd before calling cmd.Wait, as otherwise the two
	// can run into a data race.
	if err := cmd.Start(); err != nil {
		return nil, err
	}

	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case <-c.allocated: // for this browser's root context
	}
	a.wg.Add(1) // for the entire allocator
	go func() {
		// First wait for the process to be finished.
		// 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 {
			// Sometimes files/directories are still created in the user data
			// directory at this point. I can not reproduce it with strace, so
			// the reason is unknown yet. As a workaround, we will just wait a
			// little while before removing the directory.
			<-time.After(10 * time.Millisecond)
			if err := os.RemoveAll(dataDir); c.cancelErr == nil {
				c.cancelErr = err
			}
		}
		a.wg.Done()
		close(c.allocated)
	}()

	var wsURL string
	wsURLChan := make(chan struct{})
	var copy func()
	go func() {
		wsURL, copy, err = readOutput(stdout, a.combinedOutputWriter)
		close(wsURLChan)
	}()
	select {
	case <-wsURLChan:
		if err != nil {
			return nil, err
		}

	case <-time.After(a.wsURLReadTimeout):
		return nil, errors.New("websocket url timeout reached")
	}

	if a.combinedOutputWriter != nil && copy != nil {
		a.wg.Go(func() {
			copy()
		})
	}

	browser, err := NewBrowser(ctx, wsURL, opts...)
	if err != nil {
		return nil, err
	}
	go func() {
		// If the browser loses connection, kill the entire process and
		// handler at once. Don't use Cancel, as that will attempt to
		// gracefully close the browser, which will hang.
		// Don't cancel if we're in the middle of a graceful Close,
		// since we want to let Chrome shut itself when it is fully
		// finished.
		<-browser.LostConnection
		select {
		case <-browser.closingGracefully:
		default:
			c.cancel()
		}
	}()
	browser.process = cmd.Process
	browser.userDataDir = dataDir
	return browser, nil
}

// readOutput grabs the websocket address from chrome's output, returning as
// soon as it is found. All read output is forwarded to forward, if non-nil.
// done is used to signal that the asynchronous io.Copy is done, if any.
func readOutput(rc io.ReadCloser, forward io.Writer) (wsURL string, _ func(), _ error) {
	prefix := []byte("DevTools listening on")
	var accumulated bytes.Buffer
	bufr := bufio.NewReader(rc)
readLoop:
	for {
		line, err := bufr.ReadBytes('\n')
		if err != nil {
			return "", nil, fmt.Errorf("chrome failed to start:\n%s",
				accumulated.Bytes())
		}
		if forward != nil {
			if _, err := forward.Write(line); err != nil {
				return "", nil, err
			}
		}

		if bytes.HasPrefix(line, prefix) {
			line = line[len(prefix):]
			// use TrimSpace, to also remove \r on Windows
			line = bytes.TrimSpace(line)
			wsURL = string(line)
			break readLoop
		}
		accumulated.Write(line)
	}
	copy := func() {}
	if forward == nil {
		// We don't need the process's output anymore.
		rc.Close()
	} else {
		// Return a function that will be called later to
		// copy the rest of the output in a separate goroutine, as we
		// need to return with the websocket URL.
		copy = func() { io.Copy(forward, bufr) }
	}
	return wsURL, copy, nil
}

// Wait satisfies the Allocator interface.
func (a *ExecAllocator) Wait() {
	a.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(a *ExecAllocator) {
		// Convert to an absolute path if possible, to avoid
		// repeated LookPath calls in each Allocate.
		if fullPath, _ := exec.LookPath(path); fullPath != "" {
			a.execPath = fullPath
		} else {
			a.execPath = path
		}
	}
}

// findExecPath tries to find the Chrome browser somewhere in the current
// system. It finds in different locations on different OS systems.
// It could perform a rather aggressive search. That may make it a bit slow,
// but it will only be run when creating a new ExecAllocator.
func findExecPath() string {
	var locations []string
	switch runtime.GOOS {
	case "darwin":
		locations = []string{
			// Mac
			"/Applications/Chromium.app/Contents/MacOS/Chromium",
			"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
		}
	case "windows":
		locations = []string{
			// Windows
			"chrome",
			"chrome.exe", // in case PATHEXT is misconfigured
			`C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`,
			`C:\Program Files\Google\Chrome\Application\chrome.exe`,
			filepath.Join(os.Getenv("USERPROFILE"), `AppData\Local\Google\Chrome\Application\chrome.exe`),
			filepath.Join(os.Getenv("USERPROFILE"), `AppData\Local\Chromium\Application\chrome.exe`),
		}
	default:
		locations = []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",
			"/usr/local/bin/chrome",
			"/snap/bin/chromium",
			"chrome",
		}
	}

	for _, path := range locations {
		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 any) ExecAllocatorOption {
	return func(a *ExecAllocator) {
		a.initFlags[name] = value
	}
}

// Env is a list of generic environment variables in the form NAME=value
// to pass into the new Chrome process. These will be appended to the
// environment of the Go process as retrieved by os.Environ.
func Env(vars ...string) ExecAllocatorOption {
	return func(a *ExecAllocator) {
		a.initEnv = append(a.initEnv, vars...)
	}
}

// ModifyCmdFunc allows for running an arbitrary function on the
// browser exec.Cmd object. This overrides the default version
// of the command which sends SIGKILL to any open browsers when
// the Go program exits.
func ModifyCmdFunc(f func(cmd *exec.Cmd)) ExecAllocatorOption {
	return func(a *ExecAllocator) {
		a.modifyCmdFunc = f
	}
}

// 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)
}

// IgnoreCertErrors is the command line option to ignore certificate-related
// errors. This option is useful when you need to access an HTTPS website
// through a proxy.
func IgnoreCertErrors(a *ExecAllocator) {
	Flag("ignore-certificate-errors", true)(a)
}

// 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 command line option to disable the sandbox.
func NoSandbox(a *ExecAllocator) {
	Flag("no-sandbox", true)(a)
}

// NoFirstRun is the Chrome command line option to disable the first run
// dialog.
func NoFirstRun(a *ExecAllocator) {
	Flag("no-first-run", true)(a)
}

// NoDefaultBrowserCheck is the Chrome command line option to disable the
// default browser check.
func NoDefaultBrowserCheck(a *ExecAllocator) {
	Flag("no-default-browser-check", true)(a)
}

// Headless is the command line option to run in headless mode. On top of
// setting the headless flag, it also hides scrollbars and mutes audio.
func Headless(a *ExecAllocator) {
	Flag("headless", true)(a)
	// Like in Puppeteer.
	Flag("hide-scrollbars", true)(a)
	Flag("mute-audio", true)(a)
}

// DisableGPU is the command line option to disable the GPU process.
//
// The --disable-gpu option is a temporary workaround for a few bugs
// in headless mode. According to the references below, it's no longer required:
//   - https://bugs.chromium.org/p/chromium/issues/detail?id=737678
//   - https://github.com/puppeteer/puppeteer/pull/2908
//   - https://github.com/puppeteer/puppeteer/pull/4523
//
// But according to this reported issue, it's still required in some cases:
//   - https://github.com/chromedp/chromedp/issues/904
//
// Chromium 139+ doesn't provide fallback to Swiftshader unless the
// --enable-unsafe-swiftshader option is passed:
//   - https://chromestatus.com/feature/5166674414927872
func DisableGPU(a *ExecAllocator) {
	Flag("disable-gpu", true)(a)
	Flag("enable-unsafe-swiftshader", true)(a)
}

// CombinedOutput is used to set an io.Writer where stdout and stderr
// from the browser will be sent
func CombinedOutput(w io.Writer) ExecAllocatorOption {
	return func(a *ExecAllocator) {
		a.combinedOutputWriter = w
	}
}

// WSURLReadTimeout sets the waiting time for reading the WebSocket URL.
// The default value is 20 seconds.
func WSURLReadTimeout(t time.Duration) ExecAllocatorOption {
	return func(a *ExecAllocator) {
		a.wsURLReadTimeout = t
	}
}

// NewRemoteAllocator creates a new context set up with a RemoteAllocator,
// suitable for use with NewContext. The url should point to the browser's
// websocket address, such as "ws://127.0.0.1:$PORT/devtools/browser/...".
//
// If the url does not contain "/devtools/browser/", it will try to detect
// the correct one by sending a request to "http://$HOST:$PORT/json/version".
//
// The url with the following formats are accepted:
//   - ws://127.0.0.1:9222/
//   - http://127.0.0.1:9222/
//
// But "ws://127.0.0.1:9222/devtools/browser/" are not accepted.
// Because the allocator won't try to modify it and it's obviously invalid.
//
// Use chromedp.NoModifyURL to prevent it from modifying the url.
func NewRemoteAllocator(parent context.Context, url string, opts ...RemoteAllocatorOption) (context.Context, context.CancelFunc) {
	a := &RemoteAllocator{
		wsURL:         url,
		modifyURLFunc: modifyURL,
	}
	for _, o := range opts {
		o(a)
	}
	c := &Context{Allocator: a}

	ctx, cancel := context.WithCancel(parent)
	ctx = context.WithValue(ctx, contextKey{}, c)
	return ctx, cancel
}

// RemoteAllocatorOption is a remote allocator option.
type RemoteAllocatorOption = func(*RemoteAllocator)

// RemoteAllocator is an Allocator which connects to an already running Chrome
// process via a websocket URL.
type RemoteAllocator struct {
	wsURL         string
	modifyURLFunc func(ctx context.Context, wsURL string) (string, error)

	wg sync.WaitGroup
}

// Allocate satisfies the Allocator interface.
func (a *RemoteAllocator) Allocate(ctx context.Context, opts ...BrowserOption) (*Browser, error) {
	c := FromContext(ctx)
	if c == nil {
		return nil, ErrInvalidContext
	}

	wsURL := a.wsURL
	var err error
	if a.modifyURLFunc != nil {
		wsURL, err = a.modifyURLFunc(ctx, wsURL)
		if err != nil {
			return nil, fmt.Errorf("failed to modify wsURL: %w", err)
		}
	}

	// Use a different context for the websocket, so we can have a chance at
	// closing the relevant pages before closing the websocket connection.
	wctx, cancel := context.WithCancel(context.Background())

	close(c.allocated)
	// for the entire allocator
	a.wg.Go(func() {
		<-ctx.Done()
		Cancel(ctx) // block until all pages are closed
		cancel()    // close the websocket connection
	})

	browser, err := NewBrowser(wctx, wsURL, opts...)
	if err != nil {
		return nil, err
	}
	go func() {
		// If the browser loses connection, kill the entire process and
		// handler at once.
		<-browser.LostConnection
		select {
		case <-browser.closingGracefully:
		default:
			Cancel(ctx)
		}
	}()
	return browser, nil
}

// Wait satisfies the Allocator interface.
func (a *RemoteAllocator) Wait() {
	a.wg.Wait()
}

// NoModifyURL is a RemoteAllocatorOption that prevents the remote allocator
// from modifying the websocket debugger URL passed to it.
func NoModifyURL(a *RemoteAllocator) {
	a.modifyURLFunc = nil
}


================================================
FILE: allocate_linux.go
================================================
//go:build linux
// +build linux

package chromedp

import (
	"os"
	"os/exec"
	"syscall"
)

func allocateCmdOptions(cmd *exec.Cmd) {
	if _, ok := os.LookupEnv("LAMBDA_TASK_ROOT"); ok {
		// do nothing on AWS Lambda
		return
	}
	if cmd.SysProcAttr == nil {
		cmd.SysProcAttr = new(syscall.SysProcAttr)
	}
	// When the parent process dies (Go), kill the child as well.
	cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL
}


================================================
FILE: allocate_other.go
================================================
//go:build !linux
// +build !linux

package chromedp

import "os/exec"

func allocateCmdOptions(cmd *exec.Cmd) {
}


================================================
FILE: allocate_test.go
================================================
package chromedp

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"
)

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("want %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 TestExecAllocatorCombinedOutputPanic(t *testing.T) {
	t.Parallel()

	buf := new(bytes.Buffer)
	allocCtx, cancel := NewExecAllocator(context.Background(),
		append([]ExecAllocatorOption{
			CombinedOutput(buf),
			Flag("enable-logging", true),
			WSURLReadTimeout(1), // trigger err
		}, allocOpts...)...)
	defer cancel()

	ctx, _ := NewContext(allocCtx, browserOpts...)

	if _, err := FromContext(ctx).Allocator.Allocate(ctx, WithDialTimeout(1)); err != nil &&
		err.Error() != "websocket url timeout reached" &&
		!strings.Contains(err.Error(), "i/o timeout") {
		t.Fatal(err)
	}

	// give time for the `readOutput` goroutine to finish
	// this can vary depending on the system, so we give it a bit more time
	time.Sleep(5 * time.Second)

	cancel()
	// dir cleanup occurs after 10 milliseconds so this gives a bit more time
	// for the cleanup, otherwise the test may fail with a panic about the
	// directory not being removed
	time.Sleep(20 * time.Millisecond)
}

func TestExecAllocatorKillBrowser(t *testing.T) {
	t.Parallel()

	// Simulate a scenario where we navigate to a page that never responds,
	// and the browser is killed while it's loading.
	ctx, _ := testAllocateSeparate(t)
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()

	kill := make(chan struct{}, 1)
	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		kill <- struct{}{}
		<-ctx.Done() // block until the end of the test
	}))
	defer s.Close()
	go func() {
		<-kill
		b := FromContext(ctx).Browser
		if err := b.process.Signal(os.Kill); err != nil {
			t.Error(err)
		}
	}()

	// Run should error with something other than "deadline exceeded" in
	// much less than 3s.
	switch err := Run(ctx, Navigate(s.URL)); err {
	case nil:
		// TODO: figure out why this happens sometimes on Travis
		// t.Fatal("did not expect a nil error")
	case context.DeadlineExceeded:
		t.Fatalf("did not expect a standard context error: %v", err)
	}
}

func TestSkipNewContext(t *testing.T) {
	t.Parallel()

	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)
	}
}

func TestRemoteAllocator(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name      string
		modifyURL func(wsURL string) string
		opts      []RemoteAllocatorOption
		wantErr   string
	}{
		{
			name:      "original wsURL",
			modifyURL: func(wsURL string) string { return wsURL },
		},
		{
			name: "detect from ws",
			modifyURL: func(wsURL string) string {
				return wsURL[0:strings.Index(wsURL, "devtools")]
			},
		},
		{
			name: "detect from http",
			modifyURL: func(wsURL string) string {
				return "http" + wsURL[2:strings.Index(wsURL, "devtools")]
			},
		},
		{
			name: "hostname",
			modifyURL: func(wsURL string) string {
				h, err := os.Hostname()
				if err != nil {
					t.Fatal(err)
				}
				u, err := url.Parse(wsURL)
				if err != nil {
					t.Fatal(err)
				}
				_, port, err := net.SplitHostPort(u.Host)
				if err != nil {
					t.Fatal(err)
				}
				u.Host = net.JoinHostPort(h, port)
				u.Path = "/"
				return u.String()
			},
		},
		{
			name: "NoModifyURL",
			modifyURL: func(wsURL string) string {
				return wsURL[0:strings.Index(wsURL, "devtools")]
			},
			opts:    []RemoteAllocatorOption{NoModifyURL},
			wantErr: "could not dial",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			testRemoteAllocator(t, test.modifyURL, test.wantErr, test.opts)
		})
	}
}

func testRemoteAllocator(t *testing.T, modifyURL func(wsURL string) string, wantErr string, opts []RemoteAllocatorOption) {
	tempDir := t.TempDir()

	procCtx, procCancel := context.WithCancel(context.Background())
	defer procCancel()
	cmd := exec.CommandContext(procCtx, execPath,
		// TODO: deduplicate these with allocOpts in chromedp_test.go
		"--no-first-run",
		"--no-default-browser-check",
		"--headless",
		"--disable-gpu",
		"--no-sandbox",

		// TODO: perhaps deduplicate this code with ExecAllocator
		"--user-data-dir="+tempDir,
		"--remote-debugging-address=0.0.0.0",
		"--remote-debugging-port=0",
		"about:blank",
	)

	stderr, err := cmd.StderrPipe()
	if err != nil {
		t.Fatal(err)
	}
	defer stderr.Close()
	if err := cmd.Start(); err != nil {
		t.Fatal(err)
	}
	wsURL, _, err := readOutput(stderr, nil)
	if err != nil {
		t.Fatal(err)
	}
	allocCtx, allocCancel := NewRemoteAllocator(context.Background(), modifyURL(wsURL), opts...)
	defer allocCancel()

	taskCtx, taskCancel := NewContext(allocCtx,
		// This used to crash when used with RemoteAllocator.
		WithLogf(func(format string, args ...any) {}),
	)

	{
		infos, err := Targets(taskCtx)
		if len(wantErr) > 0 {
			if err == nil || !strings.Contains(err.Error(), wantErr) {
				t.Fatalf("\ngot error:\n\t%v\nwant error contains:\n\t%s", err, wantErr)
			}

			procCancel()
			cmd.Wait()
			return
		}
		if err != nil {
			t.Fatal(err)
		}
		if len(infos) > 1 {
			t.Fatalf("expected Targets on a new RemoteAllocator context to return at most one, got: %d", len(infos))
		}
	}

	defer taskCancel()
	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("want %q, got %q", want, got)
	}
	targetID := FromContext(taskCtx).Target.TargetID
	if err := Cancel(taskCtx); err != nil {
		t.Fatal(err)
	}

	// Check that cancel closed the tabs. Don't just count the
	// number of targets, as perhaps the initial blank tab hasn't
	// come up yet.
	targetsCtx, targetsCancel := NewContext(allocCtx)
	defer targetsCancel()
	infos, err := Targets(targetsCtx)
	if err != nil {
		t.Fatal(err)
	}
	for _, info := range infos {
		if info.TargetID == targetID {
			t.Fatalf("target from previous iteration wasn't closed: %v", targetID)
		}
	}
	targetsCancel()

	// Finally, if we kill the browser and the websocket connection drops,
	// Run should error way before the 5s timeout.
	// TODO: a "defer cancel()" here adds a 1s timeout, since we try to
	// close the target twice. Fix that.
	ctx, _ := NewContext(allocCtx)
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	// Connect to the browser, then kill it.
	if err := Run(ctx); err != nil {
		t.Fatal(err)
	}
	procCancel()
	switch err := Run(ctx, Navigate(testdataDir+"/form.html")); err {
	case nil:
		// TODO: figure out why this happens sometimes on Travis
		// t.Fatal("did not expect a nil error")
	case context.DeadlineExceeded:
		t.Fatalf("did not expect a standard context error: %v", err)
	}
	cmd.Wait()
}

func TestExecAllocatorMissingWebsocketAddr(t *testing.T) {
	t.Parallel()

	allocCtx, cancel := NewExecAllocator(context.Background(),
		// Use a bad listen address, so Chrome exits straight away.
		append([]ExecAllocatorOption{Flag("remote-debugging-address", "_")},
			allocOpts...)...)
	defer cancel()

	ctx, cancel := NewContext(allocCtx)
	defer cancel()

	// set the "s" flag to let "." match "\n"
	// in GitHub Actions, the error text could be:
	// "chrome failed to start:\n/bin/bash: /etc/profile.d/env_vars.sh: Permission denied\nmkdir: cannot create directory ‘/run/user/1001’: Permission denied\n[0321/081807.491906:ERROR:headless_shell.cc(720)] Invalid devtools server address\n"
	want := `failed to start`
	got := fmt.Sprintf("%v", Run(ctx))
	if !strings.Contains(got, want) {
		t.Fatalf("want error to match %q, got %q", want, got)
	}
}

func TestCombinedOutput(t *testing.T) {
	t.Parallel()

	buf := new(bytes.Buffer)
	allocCtx, cancel := NewExecAllocator(context.Background(),
		append([]ExecAllocatorOption{
			CombinedOutput(buf),
			Flag("enable-logging", true),
		}, allocOpts...)...)
	defer cancel()

	taskCtx, _ := NewContext(allocCtx)
	if err := Run(taskCtx,
		Navigate(testdataDir+"/consolespam.html"),
	); err != nil {
		t.Fatal(err)
	}
	cancel()
	if !strings.Contains(buf.String(), "DevTools listening on") {
		t.Fatalf("failed to find websocket string in browser output test")
	}
	// Recent chrome versions have started replacing many "spam" messages
	// with "spam 1", "spam 2", and so on. Search for the prefix only.
	if want, got := 2000, strings.Count(buf.String(), `"spam`); want != got {
		t.Fatalf("want %d spam console logs, got %d", want, got)
	}
}

func TestCombinedOutputError(t *testing.T) {
	t.Parallel()

	// CombinedOutput used to hang the allocator if Chrome errored straight
	// away, as there was no output to copy and the CombinedOutput would
	// never signal it's done.
	buf := new(bytes.Buffer)
	allocCtx, cancel := NewExecAllocator(context.Background(),
		// Use a bad listen address, so Chrome exits straight away.
		append([]ExecAllocatorOption{
			Flag("remote-debugging-address", "_"),
			CombinedOutput(buf),
		}, allocOpts...)...)
	defer cancel()

	ctx, cancel := NewContext(allocCtx)
	defer cancel()
	got := fmt.Sprint(Run(ctx))
	want := "failed to start"
	if !strings.Contains(got, want) {
		t.Fatalf("got %q, want %q", got, want)
	}
}

func TestEnv(t *testing.T) {
	t.Parallel()

	tz := "Australia/Melbourne"
	allocCtx, cancel := NewExecAllocator(context.Background(),
		append([]ExecAllocatorOption{
			Env("TZ=" + tz),
		}, allocOpts...)...)
	defer cancel()

	ctx, cancel := NewContext(allocCtx)
	defer cancel()

	var ret string
	if err := Run(ctx,
		Evaluate(`Intl.DateTimeFormat().resolvedOptions().timeZone`, &ret),
	); err != nil {
		t.Fatal(err)
	}

	if ret != tz {
		t.Fatalf("got %s, want %s", ret, tz)
	}
}

func TestWithBrowserOptionAlreadyAllocated(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocateSeparate(t)
	defer cancel()

	defer func() {
		want := "when allocating a new browser"
		if got := fmt.Sprint(recover()); !strings.Contains(got, want) {
			t.Errorf("expected a panic containing %q, got %q", want, got)
		}
	}()
	// This needs to panic, as we try to set up a browser logf function
	// after the browser has already been set up earlier.
	_, _ = NewContext(ctx,
		WithLogf(func(format string, args ...any) {}),
	)
}

func TestModifyCmdFunc(t *testing.T) {
	t.Parallel()

	tz := "Atlantic/Reykjavik"
	allocCtx, cancel := NewExecAllocator(context.Background(),
		append([]ExecAllocatorOption{
			ModifyCmdFunc(func(cmd *exec.Cmd) {
				cmd.Env = append(cmd.Env, "TZ="+tz)
			}),
		}, allocOpts...)...)
	defer cancel()

	ctx, cancel := NewContext(allocCtx)
	defer cancel()

	var ret string
	if err := Run(ctx,
		Evaluate(`Intl.DateTimeFormat().resolvedOptions().timeZone`, &ret),
	); err != nil {
		t.Fatal(err)
	}

	if ret != tz {
		t.Fatalf("got %s, want %s", ret, tz)
	}
}

// TestStartsWithNonBlankTab is a regression test to make sure chromedp won't
// hang when the browser is started with a non-blank tab.
//
// In the following cases, the browser will start with a non-blank tab:
// 1. with the "--app" option (should disable headless mode);
// 2. URL other than "about:blank" is placed in the command line arguments.
//
// It's hard to disable headless mode on test servers, so we will go with
// case 2 here.
func TestStartsWithNonBlankTab(t *testing.T) {
	t.Parallel()

	allocCtx, cancel := NewExecAllocator(context.Background(),
		append(allocOpts,
			ModifyCmdFunc(func(cmd *exec.Cmd) {
				// it assumes that the last argument is "about:blank" and
				// replace it with other URL.
				cmd.Args[len(cmd.Args)-1] = testdataDir + "/form.html"
			}),
		)...)
	defer cancel()

	ctx, cancel := NewContext(allocCtx)
	defer cancel()

	ctx, cancel = context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	if err := Run(ctx,
		Navigate(testdataDir+"/form.html"),
	); err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			t.Error("chromedp hangs when the browser starts with a non-blank tab.")
		} else {
			t.Errorf("got error %s, want nil", err)
		}
	}
}


================================================
FILE: browser.go
================================================
package chromedp

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"sync"
	"sync/atomic"
	"time"

	"github.com/chromedp/cdproto"
	"github.com/chromedp/cdproto/browser"
	"github.com/chromedp/cdproto/cdp"
	"github.com/chromedp/cdproto/runtime"
	"github.com/chromedp/cdproto/target"
	jsonv2 "github.com/go-json-experiment/json"
	"github.com/go-json-experiment/json/jsontext"
)

var (
	// DefaultUnmarshalOptions are default unmarshal options.
	DefaultUnmarshalOptions = jsonv2.JoinOptions(
		jsonv2.DefaultOptionsV2(),
		jsontext.AllowInvalidUTF8(true),
	)
	// DefaultMarshalOptions are default marshal options.
	DefaultMarshalOptions = jsonv2.JoinOptions(
		jsonv2.DefaultOptionsV2(),
		jsontext.AllowInvalidUTF8(true),
	)
)

// 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 {
	// next is the next message id.
	// NOTE: needs to be 64-bit aligned for 32-bit targets too, so be careful when moving this field.
	// This will be eventually done by the compiler once https://github.com/golang/go/issues/599 is fixed.
	next int64

	// LostConnection is closed when the websocket connection to Chrome is
	// dropped. This can be useful to make sure that Browser's context is
	// cancelled (and the handler stopped) once the connection has failed.
	LostConnection chan struct{}

	// closingGracefully is closed by Close before gracefully shutting down
	// the browser. This way, when the connection to the browser is lost and
	// LostConnection is closed, we will know not to immediately kill the
	// Chrome process. This is important to let the browser shut itself off,
	// saving its state to disk.
	closingGracefully chan struct{}

	dialTimeout time.Duration

	// pages keeps track of the attached targets, indexed by each's session
	// ID. The only reason this is a field is so that the tests can check the
	// map once a browser is closed.
	pages map[target.SessionID]*Target

	listenersMu sync.Mutex
	listeners   []cancelableListener

	conn Transport

	// newTabQueue 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 newTabResult.
	newTabQueue chan *Target

	// cmdQueue is the outgoing command queue.
	cmdQueue chan *cdproto.Message

	// logging funcs
	logf func(string, ...any)
	errf func(string, ...any)
	dbgf func(string, ...any)

	// 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
}

// NewBrowser creates a new browser. Typically, this function wouldn't be called
// directly, as the Allocator interface takes care of it.
func NewBrowser(ctx context.Context, urlstr string, opts ...BrowserOption) (*Browser, error) {
	b := &Browser{
		LostConnection:    make(chan struct{}),
		closingGracefully: make(chan struct{}),

		dialTimeout: 10 * time.Second,

		newTabQueue: make(chan *Target),

		// Fit some jobs without blocking, to reduce blocking in Execute.
		cmdQueue: make(chan *cdproto.Message, 32),

		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 ...any) { b.logf("ERROR: "+s, v...) }
	}

	dialCtx := ctx
	if b.dialTimeout > 0 {
		var cancel context.CancelFunc
		dialCtx, cancel = context.WithTimeout(ctx, b.dialTimeout)
		defer cancel()
	}

	var err error
	b.conn, err = DialContext(dialCtx, urlstr, WithConnDebugf(b.dbgf))
	if err != nil {
		return nil, fmt.Errorf("could not dial %q: %w", urlstr, err)
	}

	go b.run(ctx)
	return b, nil
}

// Process returns the process object of the browser.
//
// It could be nil when the browser is allocated with RemoteAllocator.
// It could be useful for a monitoring system to collect process metrics of the browser process.
// (See [prometheus.NewProcessCollector] for an example).
//
// Example:
//
//	if process := chromedp.FromContext(ctx).Browser.Process(); process != nil {
//		fmt.Printf("Browser PID: %v", process.Pid)
//	}
//
// [prometheus.NewProcessCollector]: https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#NewProcessCollector
func (b *Browser) Process() *os.Process {
	return b.process
}

func (b *Browser) newExecutorForTarget(ctx context.Context, targetID target.ID, sessionID target.SessionID) (*Target, error) {
	if targetID == "" {
		return nil, errors.New("empty target ID")
	}
	if sessionID == "" {
		return nil, errors.New("empty session ID")
	}
	t := &Target{
		browser:   b,
		TargetID:  targetID,
		SessionID: sessionID,

		messageQueue: make(chan *cdproto.Message, 1024),
		frames:       make(map[cdp.FrameID]*cdp.Frame),
		execContexts: make(map[cdp.FrameID]runtime.ExecutionContextID),
		cur:          cdp.FrameID(targetID),

		logf: b.logf,
		errf: b.errf,
	}

	// This send should be blocking, to ensure the tab is inserted into the
	// map before any more target events are routed.
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case b.newTabQueue <- t:
	}
	return t, nil
}

func (b *Browser) Execute(ctx context.Context, method string, params, res any) error {
	// Certain methods aren't available to the user directly.
	if method == browser.CommandClose {
		return fmt.Errorf("to close the browser gracefully, use chromedp.Cancel")
	}
	return b.execute(ctx, method, params, res)
}

func (b *Browser) execute(ctx context.Context, method string, params, res any) error {
	id := atomic.AddInt64(&b.next, 1)
	lctx, cancel := context.WithCancel(ctx)
	ch := make(chan *cdproto.Message, 1)
	fn := func(ev any) {
		if msg, ok := ev.(*cdproto.Message); ok && msg.ID == id {
			select {
			case <-ctx.Done():
			case ch <- msg:
			}
			cancel()
		}
	}
	b.listenersMu.Lock()
	b.listeners = append(b.listeners, cancelableListener{lctx, fn})
	b.listenersMu.Unlock()

	// send command
	var buf []byte
	if params != nil {
		var err error
		if buf, err = jsonv2.Marshal(params, DefaultMarshalOptions); err != nil {
			return err
		}
	}
	cmd := &cdproto.Message{
		ID:     id,
		Method: cdproto.MethodType(method),
		Params: buf,
	}
	select {
	case <-ctx.Done():
		return ctx.Err()
	case b.cmdQueue <- cmd:
	}

	// wait for result
	select {
	case <-ctx.Done():
		return ctx.Err()
	case msg := <-ch:
		switch {
		case msg == nil:
			return ErrChannelClosed
		case msg.Error != nil:
			return msg.Error
		case res != nil:
			return jsonv2.Unmarshal(msg.Result, res, DefaultUnmarshalOptions)
		}
	}
	return nil
}

func (b *Browser) run(ctx context.Context) {
	defer b.conn.Close()

	// incomingQueue is the queue of incoming target events, to be routed by
	// their session ID.
	incomingQueue := make(chan *cdproto.Message, 1)

	delTabQueue := make(chan target.SessionID, 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() {
		// Signal to run and exit the browser cleanup goroutine.
		defer close(b.LostConnection)

		for {
			msg := new(cdproto.Message)
			if err := b.conn.Read(ctx, msg); err != nil {
				var syntacticError *jsontext.SyntacticError
				if errors.As(err, &syntacticError) {
					b.errf("%s", err)
				}
				return
			}

			switch {
			case msg.SessionID != "" && (msg.Method != "" || msg.ID != 0):
				select {
				case <-ctx.Done():
					return
				case incomingQueue <- msg:
				}

			case msg.Method != "":
				ev, err := cdproto.UnmarshalMessage(msg, DefaultUnmarshalOptions)
				if err != nil {
					b.errf("%s", err)
					continue
				}
				b.listenersMu.Lock()
				b.listeners = runListeners(b.listeners, ev)
				b.listenersMu.Unlock()

				if ev, ok := ev.(*target.EventDetachedFromTarget); ok {
					delTabQueue <- ev.SessionID
				}

			case msg.ID != 0:
				b.listenersMu.Lock()
				b.listeners = runListeners(b.listeners, msg)
				b.listenersMu.Unlock()

			default:
				b.errf("ignoring malformed incoming message (missing id or method): %#v", msg)
			}
		}
	}()

	b.pages = make(map[target.SessionID]*Target, 32)
	for {
		select {
		case <-ctx.Done():
			return

		case msg := <-b.cmdQueue:
			if err := b.conn.Write(ctx, msg); err != nil {
				b.errf("%s", err)
				continue
			}

		case t := <-b.newTabQueue:
			if _, ok := b.pages[t.SessionID]; ok {
				b.errf("executor for %q already exists", t.SessionID)
			}
			b.pages[t.SessionID] = t

		case sessionID := <-delTabQueue:
			if _, ok := b.pages[sessionID]; !ok {
				b.errf("executor for %q doesn't exist", sessionID)
			}
			delete(b.pages, sessionID)

		case m := <-incomingQueue:
			page, ok := b.pages[m.SessionID]
			if !ok {
				// A page we recently closed still sending events.
				continue
			}

			select {
			case <-ctx.Done():
				return
			case page.messageQueue <- m:
			}

		case <-b.LostConnection:
			return // to avoid "write: broken pipe" errors
		}
	}
}

// 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, ...any)) 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, ...any)) 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, ...any)) 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, ...any)) BrowserOption {
	return func(b *Browser) {}
}

// WithDialTimeout is a browser option to specify the timeout when dialing a
// browser's websocket address. The default is ten seconds; use a zero duration
// to not use a timeout.
func WithDialTimeout(d time.Duration) BrowserOption {
	return func(b *Browser) { b.dialTimeout = d }
}


================================================
FILE: browser_test.go
================================================
package chromedp

import (
	"bytes"
	"github.com/chromedp/cdproto"
	jsonv2 "github.com/go-json-experiment/json"
	"github.com/go-json-experiment/json/jsontext"
	"testing"
)

func TestUnmarshalWithDefaultOptions(t *testing.T) {
	tests := []struct {
		Name      string
		Input     []byte
		WantError bool
	}{
		{Name: "simple json", Input: []byte(`{"id":1, "result": {"bar":"foo"}}`), WantError: false},
		{Name: "invalid utf-8 1", Input: []byte(`{"id":2, "result": "\udebe\\u018e8"}`), WantError: false},
		{Name: "invalid utf-8 2", Input: []byte(`{"id":2, "result": "y7vzPw=T6\u001a\u053a{\ud861=\u001b"}`), WantError: false},
		{Name: "empty json", Input: []byte(`{}`), WantError: false},
		{Name: "null result", Input: []byte(`{"id":4, "result": null}`), WantError: false},
		{Name: "nested json", Input: []byte(`{"id":5, "result": {"nested": {"foo": "bar"}}}`), WantError: false},
		{Name: "array in result", Input: []byte(`{"id":6, "result": ["foo", "bar"]}`), WantError: false},
		{Name: "boolean in result", Input: []byte(`{"id":7, "result": true}`), WantError: false},
		{Name: "number in result", Input: []byte(`{"id":8, "result": 12345}`), WantError: false},
		{Name: "string in result", Input: []byte(`{"id":9, "result": "foobar"}`), WantError: false},
		{Name: "extra fields", Input: []byte(`{"id":10, "result": {"bar":"foo"}, "extra": "field"}`), WantError: false},
		{Name: "invalid json", Input: []byte(`{"id":11, "result": {"bar":"foo"`), WantError: true},
		{Name: "empty input", Input: []byte(``), WantError: true},
		{Name: "whitespace input", Input: []byte(`   `), WantError: true},
		{Name: "null input", Input: nil, WantError: true},
	}
	for _, test := range tests {
		t.Run(test.Name, func(t *testing.T) {
			var decoder jsontext.Decoder
			var b bytes.Buffer
			b.Write(test.Input)
			var msg cdproto.Message
			decoder.Reset(&b, DefaultUnmarshalOptions)
			err := jsonv2.UnmarshalDecode(&decoder, &msg, DefaultUnmarshalOptions)
			if test.WantError && err == nil {
				t.Error("expected error in unmarshal decode, but got none")
			} else if !test.WantError && err != nil {
				t.Errorf("expected no error in unmarshal decode, but got %s", err)
			}
			err = jsonv2.Unmarshal(test.Input, &msg, DefaultUnmarshalOptions)
			if test.WantError && err == nil {
				t.Error("expected error, but got none")
			} else if !test.WantError && err != nil {
				t.Errorf("expected no error, but got %s", err)
			}
		})
	}
}

func TestMarshalWithDefaultOptions(t *testing.T) {
	tests := []struct {
		Name      string
		Input     cdproto.Message
		WantError bool
	}{
		{Name: "simple json", Input: cdproto.Message{Result: []byte(`{"bar":"foo"}`)}, WantError: false},
		{Name: "invalid utf-8 1", Input: cdproto.Message{Result: []byte(`"\udebe\\u018e8"`)}, WantError: false},
		{Name: "invalid utf-8 2", Input: cdproto.Message{Result: []byte(`"y7vzPw=T6\u001a\u053a{\ud861=\u001b"`)}, WantError: false},
	}
	for _, test := range tests {
		t.Run(test.Name, func(t *testing.T) {
			var encoder jsontext.Encoder
			var b bytes.Buffer
			encoder.Reset(&b, DefaultMarshalOptions)
			err := jsonv2.MarshalEncode(&encoder, &test.Input, DefaultMarshalOptions)
			if test.WantError && err == nil {
				t.Error("expected error in marshal encode, but got none")
			} else if !test.WantError && err != nil {
				t.Errorf("expected no error in marshal encode, but got %s", err)
			}
			_, err = jsonv2.Marshal(test.Input, DefaultMarshalOptions)
			if test.WantError && err == nil {
				t.Error("expected error, but got none")
			} else if !test.WantError && err != nil {
				t.Errorf("expected no error, but got %s", err)
			}
		})
	}
}


================================================
FILE: call.go
================================================
package chromedp

import (
	"context"

	"github.com/chromedp/cdproto/runtime"
	jsonv2 "github.com/go-json-experiment/json"
)

// CallAction are actions that calls a JavaScript function using
// runtime.CallFunctionOn.
type CallAction Action

// CallFunctionOn is an action to call a JavaScript function, unmarshaling
// the result of the function to res.
//
// The handling of res is the same as that of Evaluate.
//
// Do not call the following methods on runtime.CallFunctionOnParams:
// - WithReturnByValue: it will be set depending on the type of res;
// - WithArguments: pass the arguments with args instead.
//
// Note: any exception encountered will be returned as an error.
func CallFunctionOn(functionDeclaration string, res any, opt CallOption, args ...any) CallAction {
	return ActionFunc(func(ctx context.Context) error {
		_, err := callFunctionOn(ctx, functionDeclaration, res, opt, args...)
		return err
	})
}

func callFunctionOn(ctx context.Context, functionDeclaration string, res any, opt CallOption, args ...any) (*runtime.RemoteObject, error) {
	// set up parameters
	p := runtime.CallFunctionOn(functionDeclaration).
		WithSilent(true)

	switch res.(type) {
	case **runtime.RemoteObject:
	default:
		p = p.WithReturnByValue(true)
	}

	// apply opt
	if opt != nil {
		p = opt(p)
	}

	// arguments
	if len(args) > 0 {
		ea := &errAppender{args: make([]*runtime.CallArgument, 0, len(args))}
		for _, arg := range args {
			ea.append(arg)
		}
		if ea.err != nil {
			return nil, ea.err
		}
		p = p.WithArguments(ea.args)
	}

	// call
	v, exp, err := p.Do(ctx)
	if err != nil {
		return nil, err
	}
	if exp != nil {
		return nil, exp
	}

	return v, parseRemoteObject(v, res)
}

// CallOption is a function to modify the runtime.CallFunctionOnParams to
// provide more information.
type CallOption = func(params *runtime.CallFunctionOnParams) *runtime.CallFunctionOnParams

// errAppender is to help accumulating the arguments and simplifying error checks.
//
// see https://blog.golang.org/errors-are-values
type errAppender struct {
	args []*runtime.CallArgument
	err  error
}

// append method calls the jsonv2.Marshal method to marshal the value and
// appends it to the slice. It records the first error for future reference.
//
// As soon as an error occurs, the append method becomes a no-op but the error
// value is saved.
func (ea *errAppender) append(v any) {
	if ea.err != nil {
		return
	}
	var b []byte
	b, ea.err = jsonv2.Marshal(v, DefaultMarshalOptions)
	ea.args = append(ea.args, &runtime.CallArgument{Value: b})
}


================================================
FILE: chromedp.go
================================================
// Package chromedp is a high level Chrome DevTools Protocol client that
// simplifies driving browsers for scraping, unit testing, or profiling web
// pages using the CDP.
//
// chromedp requires no third-party dependencies, implementing the async Chrome
// DevTools Protocol entirely in Go.
//
// This package includes a number of simple examples. Additionally,
// [chromedp/examples] contains more complex examples.
//
// [chromedp/examples]: https://github.com/chromedp/examples
package chromedp

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/chromedp/cdproto/browser"
	"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/network"
	"github.com/chromedp/cdproto/page"
	"github.com/chromedp/cdproto/runtime"
	"github.com/chromedp/cdproto/target"
)

// Context is attached to any context.Context which is valid for use with Run.
type Context struct {
	// Allocator is used to create new browsers. It is inherited from the
	// parent context when using NewContext.
	Allocator Allocator

	// Browser is the browser being used in the context. It is inherited
	// from the parent context when using NewContext.
	Browser *Browser

	// 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

	// targetID is set up by WithTargetID. If nil, Run will pick the only
	// unused page target, or create a new one.
	targetID target.ID

	// createBrowserContextParams is set up by WithNewBrowserContext. It is used
	// to create a new BrowserContext.
	createBrowserContextParams *target.CreateBrowserContextParams

	// browserContextOwner indicates whether this context is a BrowserContext
	// owner. The owner is responsible for disposing the BrowserContext once
	// the context is done.
	browserContextOwner bool

	// BrowserContextID is set up by WithExistingBrowserContext.
	//
	// Otherwise, BrowserContextID holds a non-empty value in the following cases:
	//
	// 1. if the context is created with the WithNewBrowserContext option, a new
	// BrowserContext is created on its first run, and BrowserContextID holds
	// the id of that new BrowserContext;
	//
	// 2. if the context is not created with the WithTargetID option, and its
	// parent context has a non-empty BrowserContextID, this context's
	// BrowserContextID is copied from the parent context.
	BrowserContextID cdp.BrowserContextID

	browserListeners []cancelableListener
	targetListeners  []cancelableListener

	// 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

	// 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()

	// first records whether this context created a brand new Chrome
	// process. This is important, because its cancellation should stop the
	// entire browser and its handler, and not just a portion of its pages.
	first bool

	// closedTarget allows waiting for a target's page to be closed on
	// cancellation.
	closedTarget sync.WaitGroup

	// allocated is closed when an allocated browser completely stops. If no
	// browser needs to be allocated, the channel is simply not initialized
	// and remains nil.
	allocated chan struct{}

	// 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
}

// 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].
//
// Note that NewContext doesn't allocate nor start a browser; that happens the
// first time Run is used on the context.
func NewContext(parent context.Context, opts ...ContextOption) (context.Context, context.CancelFunc) {
	ctx, cancel := context.WithCancel(parent)

	c := &Context{cancel: cancel, first: true}
	var parentBrowserContextID cdp.BrowserContextID
	if pc := FromContext(parent); pc != nil {
		c.Allocator = pc.Allocator
		c.Browser = pc.Browser
		parentBrowserContextID = pc.BrowserContextID
		// don't inherit Target, so that NewContext can be used to
		// create a new tab on the same browser.

		c.first = c.Browser == nil

		// TODO: make this more generic somehow.
		if _, ok := c.Allocator.(*RemoteAllocator); ok {
			c.first = false
		}
	}
	if c.Browser == nil {
		// set up the semaphore for Allocator.Allocate
		c.allocated = make(chan struct{}, 1)
		c.allocated <- struct{}{}
	}

	for _, o := range opts {
		o(c)
	}
	if c.createBrowserContextParams != nil && c.BrowserContextID != "" {
		panic("WithExistingBrowserContext can not be used when WithNewBrowserContext is specified")
	}
	if c.targetID == "" {
		if c.BrowserContextID == "" {
			// Inherit BrowserContextID from its parent context.
			c.BrowserContextID = parentBrowserContextID
		}
	} else {
		if c.createBrowserContextParams != nil {
			panic("WithNewBrowserContext can not be used when WithTargetID is specified")
		}
		if c.BrowserContextID != "" {
			panic("WithExistingBrowserContext can not be used when WithTargetID is specified")
		}
	}

	if c.Allocator == nil {
		c.Allocator = setupExecAllocator(DefaultExecAllocatorOptions[:]...)
	}

	ctx = context.WithValue(ctx, contextKey{}, c)
	c.closedTarget.Add(1)
	go func() {
		<-ctx.Done()
		defer c.closedTarget.Done()
		if c.first {
			// This is the original browser tab, so the entire
			// browser will already be cleaned up elsewhere.
			return
		}

		if c.Target == nil {
			// This is a new tab, but we didn't create it and attach
			// to it yet. Nothing to do.
			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()
		browserExecutor := cdp.WithExecutor(ctx, c.Browser)
		if id := c.Target.SessionID; id != "" {
			action := target.DetachFromTarget().WithSessionID(id)
			if err := action.Do(browserExecutor); c.cancelErr == nil && err != nil {
				c.cancelErr = err
			}
		}
		if id := c.Target.TargetID; id != "" {
			action := target.CloseTarget(id)
			if err := action.Do(browserExecutor); c.cancelErr == nil && err != nil {
				c.cancelErr = err
			}
		}
		if c.browserContextOwner {
			action := target.DisposeBrowserContext(c.BrowserContextID)
			if err := action.Do(browserExecutor); c.cancelErr == nil && err != nil {
				c.cancelErr = err
			}
		}
	}()
	cancelWait := func() {
		cancel()
		c.closedTarget.Wait()
		// If we allocated, wait for the browser to stop.
		if c.allocated != nil {
			<-c.allocated
		}
	}
	return ctx, cancelWait
}

type contextKey struct{}

// FromContext extracts the Context data stored inside a context.Context.
func FromContext(ctx context.Context) *Context {
	c, _ := ctx.Value(contextKey{}).(*Context)
	return c
}

// Cancel cancels a chromedp context, waits for its resources to be cleaned up,
// and returns any error encountered during that process.
//
// If the context allocated a browser, the browser will be closed gracefully by
// Cancel. A timeout can be attached to this context to determine how long to
// wait for the browser to close itself:
//
//	tctx, tcancel := context.WithTimeout(ctx, 10 * time.Second)
//	defer tcancel()
//	chromedp.Cancel(tctx)
//
// Usually a "defer cancel()" will be enough for most use cases. However, Cancel
// is the better option if one wants to gracefully close a browser, or catch
// underlying errors happening during cancellation.
func Cancel(ctx context.Context) error {
	c := FromContext(ctx)
	// c.cancel is nil when Cancel is wrongly called with a context returned
	// by chromedp.NewExecAllocator or chromedp.NewRemoteAllocator.
	if c == nil || c.cancel == nil {
		return ErrInvalidContext
	}
	graceful := c.first && c.Browser != nil
	if graceful {
		close(c.Browser.closingGracefully)
		if err := c.Browser.execute(ctx, browser.CommandClose, nil, nil); err != nil {
			return err
		}
	} else {
		c.cancel()
		c.closedTarget.Wait()
	}
	// If we allocated, wait for the browser to stop, up to any possible
	// deadline set in this ctx.
	ready := false
	if c.allocated != nil {
		select {
		case <-c.allocated:
			ready = true
		case <-ctx.Done():
		}
	}
	// If this was a graceful close, cancel the entire context, in case any
	// goroutines or resources are left, or if we hit the timeout above and
	// the browser hasn't finished yet. Note that, in the non-graceful path,
	// we already called c.cancel above.
	if graceful {
		c.cancel()
	}

	// If we allocated and we hit ctx.Done earlier, we can't rely on
	// cancelErr being ready until the allocated channel is closed, as that
	// is racy. If we didn't hit ctx.Done earlier, then c.allocated was
	// already cancelled then, so this will be a no-op.
	if !ready && c.allocated != nil {
		<-c.allocated
	}
	return c.cancelErr
}

func initContextBrowser(ctx context.Context) (*Context, 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 nil, ErrInvalidContext
	}
	if c.Browser == nil {
		b, err := c.Allocator.Allocate(ctx, c.browserOpts...)
		if err != nil {
			return nil, err
		}
		c.Browser = b
		c.Browser.listeners = append(c.Browser.listeners, c.browserListeners...)
	}
	return c, nil
}

// Run runs an action against context. The provided context must be a valid
// chromedp context, typically created via NewContext.
//
// Note that the first time Run is called on a context, a browser will be
// allocated via Allocator. Thus, it's generally a bad idea to use a context
// timeout on the first Run call, as it will stop the entire browser.
//
// Also note that the actions are run with the Target executor. In the case that
// a Browser executor is required, the action can be written like this:
//
//	err := chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
//		c := chromedp.FromContext(ctx)
//		id, err := target.CreateBrowserContext().Do(cdp.WithExecutor(ctx, c.Browser))
//		return err
//	}))
func Run(ctx context.Context, actions ...Action) error {
	c, err := initContextBrowser(ctx)
	if err != nil {
		return err
	}
	if c.Target == nil {
		if err := c.newTarget(ctx); err != nil {
			return err
		}
	}
	return Tasks(actions).Do(cdp.WithExecutor(ctx, c.Target))
}

func (c *Context) newTarget(ctx context.Context) error {
	if c.targetID != "" {
		if err := c.attachTarget(ctx, c.targetID); err != nil {
			return err
		}
		// This new page might have already loaded its top-level frame
		// already, in which case we wouldn't see the frameNavigated and
		// documentUpdated events. Load them here.
		// Since at the time of writing this (2020-1-27), Page.* CDP methods are
		// not implemented in worker targets, we need to skip this step when we
		// attach to workers.
		if !c.Target.isWorker {
			tree, err := page.GetFrameTree().Do(cdp.WithExecutor(ctx, c.Target))
			if err != nil {
				return err
			}

			c.Target.frameMu.Lock()
			c.Target.frames[tree.Frame.ID] = tree.Frame
			c.Target.cur = tree.Frame.ID
			c.Target.frameMu.Unlock()

			c.Target.documentUpdated(ctx)
		}
		return nil
	}
	if !c.first {
		var err error
		browserExecutor := cdp.WithExecutor(ctx, c.Browser)
		if c.createBrowserContextParams != nil {
			c.BrowserContextID, err = c.createBrowserContextParams.Do(browserExecutor)
			if err != nil {
				return err
			}
			c.browserContextOwner = true
			c.createBrowserContextParams = nil
		}
		c.targetID, err = target.
			CreateTarget("about:blank").
			WithBrowserContextID(c.BrowserContextID).
			Do(browserExecutor)
		if err != nil {
			return err
		}
		return c.attachTarget(ctx, c.targetID)
	}

	// This is like WaitNewTarget, but for the entire browser.
	ch := make(chan target.ID, 1)
	lctx, cancel := context.WithCancel(ctx)
	ListenBrowser(lctx, func(ev any) {
		var info *target.Info
		switch ev := ev.(type) {
		case *target.EventTargetCreated:
			info = ev.TargetInfo
		case *target.EventTargetInfoChanged:
			info = ev.TargetInfo
		default:
			return
		}
		// In the following cases, the browser will start with a non-blank tab:
		// 1. with the "--app" option (should disable headless mode);
		// 2. URL other than "about:blank" is placed in the command line arguments.
		// So we should not require that the URL to be "about:blank".
		// See issue https://github.com/chromedp/chromedp/issues/1076
		// In any cases that the browser starts with multiple tabs open,
		// it should be okay to attach to any one of them (no matter whether it
		// is blank).
		if info.Type == "page" {
			select {
			case <-lctx.Done():
			case ch <- info.TargetID:
			}
			cancel()
		}
	})

	// wait for the first tab to appear
	action := target.SetDiscoverTargets(true)
	if err := action.Do(cdp.WithExecutor(ctx, c.Browser)); err != nil {
		return err
	}
	select {
	case <-ctx.Done():
		return ctx.Err()
	case c.targetID = <-ch:
	}
	return c.attachTarget(ctx, c.targetID)
}

func (c *Context) attachTarget(ctx context.Context, targetID target.ID) error {
	sessionID, err := target.AttachToTarget(targetID).WithFlatten(true).Do(cdp.WithExecutor(ctx, c.Browser))
	if err != nil {
		return err
	}

	c.Target, err = c.Browser.newExecutorForTarget(ctx, targetID, sessionID)
	if err != nil {
		return err
	}

	c.Target.listeners = append(c.Target.listeners, c.targetListeners...)
	go c.Target.run(ctx)

	// Check if this is a worker target. We cannot use Target.getTargetInfo or
	// Target.getTargets in a worker, so we check if "self" refers to a
	// WorkerGlobalScope or ServiceWorkerGlobalScope.
	if err := runtime.Enable().Do(cdp.WithExecutor(ctx, c.Target)); err != nil {
		return err
	}
	res, _, err := runtime.Evaluate("self").Do(cdp.WithExecutor(ctx, c.Target))
	if err != nil {
		return err
	}
	c.Target.isWorker = strings.Contains(res.ClassName, "WorkerGlobalScope")

	// Enable available domains and discover targets.
	actions := []Action{
		log.Enable(),
		network.Enable(),
	}
	// These actions are not available on a worker target.
	if !c.Target.isWorker {
		actions = append(actions, []Action{
			inspector.Enable(),
			page.Enable(),
			dom.Enable(),
			css.Enable(),
			target.SetDiscoverTargets(true),
			target.SetAutoAttach(true, false).WithFlatten(true),
			page.SetLifecycleEventsEnabled(true),
		}...)
	}

	for _, action := range actions {
		if err := action.Do(cdp.WithExecutor(ctx, c.Target)); err != nil {
			return fmt.Errorf("unable to execute %T: %w", action, err)
		}
	}
	return nil
}

// ContextOption is a context option.
type ContextOption = func(*Context)

// WithTargetID sets up a context to be attached to an existing target, instead
// of creating a new one.
func WithTargetID(id target.ID) ContextOption {
	return func(c *Context) { c.targetID = id }
}

// CreateBrowserContextOption is a BrowserContext creation options.
type CreateBrowserContextOption = func(*target.CreateBrowserContextParams) *target.CreateBrowserContextParams

// WithNewBrowserContext sets up a context to create a new BrowserContext, and
// create a new target in this BrowserContext. A child context will create its
// target in this BrowserContext too, unless it's set up with other options.
// The new BrowserContext will be disposed when the context is done.
func WithNewBrowserContext(options ...CreateBrowserContextOption) ContextOption {
	return func(c *Context) {
		if c.first {
			panic("WithNewBrowserContext can not be used before Browser is initialized")
		}

		params := target.CreateBrowserContext().WithDisposeOnDetach(true)
		for _, o := range options {
			params = o(params)
		}
		c.createBrowserContextParams = params
	}
}

// WithExistingBrowserContext sets up a context to create a new target in the
// specified browser context.
func WithExistingBrowserContext(id cdp.BrowserContextID) ContextOption {
	return func(c *Context) {
		if c.first {
			panic("WithExistingBrowserContext can not be used before Browser is initialized")
		}
		c.BrowserContextID = id
	}
}

// WithLogf is a shortcut for WithBrowserOption(WithBrowserLogf(f)).
func WithLogf(f func(string, ...any)) ContextOption {
	return WithBrowserOption(WithBrowserLogf(f))
}

// WithErrorf is a shortcut for WithBrowserOption(WithBrowserErrorf(f)).
func WithErrorf(f func(string, ...any)) ContextOption {
	return WithBrowserOption(WithBrowserErrorf(f))
}

// WithDebugf is a shortcut for WithBrowserOption(WithBrowserDebugf(f)).
func WithDebugf(f func(string, ...any)) ContextOption {
	return WithBrowserOption(WithBrowserDebugf(f))
}

// WithBrowserOption allows passing a number of browser options to the allocator
// when allocating a new browser. As such, this context option can only be used
// when NewContext is allocating a new browser.
func WithBrowserOption(opts ...BrowserOption) ContextOption {
	return func(c *Context) {
		if c.Browser != nil {
			panic("WithBrowserOption can only be used when allocating a new browser")
		}
		c.browserOpts = append(c.browserOpts, opts...)
	}
}

// RunResponse is an alternative to Run which can be used with a list of actions
// that trigger a page navigation, such as clicking on a link or button.
//
// RunResponse will run the actions and block until a page loads, returning the
// HTTP response information for its HTML document. This can be useful to wait
// for the page to be ready, or to catch 404 status codes, for example.
//
// Note that if the actions trigger multiple navigations, only the first is
// used. And if the actions trigger no navigations at all, RunResponse will
// block until the context is cancelled.
func RunResponse(ctx context.Context, actions ...Action) (*network.Response, error) {
	var resp *network.Response
	if err := Run(ctx, responseAction(&resp, actions...)); err != nil {
		return nil, err
	}
	return resp, nil
}

func responseAction(resp **network.Response, actions ...Action) Action {
	return ActionFunc(func(ctx context.Context) error {
		// loaderID lets us filter the requests from the currently
		// loading navigation.
		var loaderID cdp.LoaderID

		// reqID is the request we're currently looking at. This can
		// go through multiple values, e.g. if the page redirects.
		var reqID network.RequestID

		// frameID corresponds to the target's root frame.
		var frameID cdp.FrameID

		var loadErr error
		hasInit := false
		finished := false

		// First, set up the function to handle events.
		// We are listening for lifecycle events, so we will use those to
		// make sure we grab the response for a request initiated by the
		// loaderID that we want.

		lctx, lcancel := context.WithCancel(ctx)
		defer lcancel()
		handleEvent := func(ev any) {
			switch ev := ev.(type) {
			case *network.EventRequestWillBeSent:
				if ev.LoaderID == loaderID && ev.Type == network.ResourceTypeDocument {
					reqID = ev.RequestID
				}
			case *network.EventLoadingFailed:
				if ev.RequestID == reqID {
					loadErr = fmt.Errorf("page load error %s", ev.ErrorText)
					// If Canceled is true, we won't receive a
					// loadEventFired at all.
					if ev.Canceled {
						finished = true
						lcancel()
					}
				}
			case *network.EventResponseReceived:
				if ev.RequestID == reqID && resp != nil {
					*resp = ev.Response
				}
			case *page.EventLifecycleEvent:
				if ev.FrameID == frameID && ev.Name == "init" {
					hasInit = true
				}
			case *page.EventLoadEventFired:
				// Ignore load events before the "init"
				// lifecycle event, as those are old.
				if hasInit {
					finished = true
					lcancel()
				}
			case *page.EventFrameStoppedLoading:
				if hasInit && ev.FrameID == frameID {
					finished = true
					lcancel()
				}
			}
		}
		// earlyEvents is a buffered list of events which happened
		// before we knew what loaderID to look for.
		var earlyEvents []any

		// Obtain frameID from the target.
		c := FromContext(ctx)
		c.Target.frameMu.RLock()
		frameID = c.Target.cur
		c.Target.frameMu.RUnlock()

		ListenTarget(lctx, func(ev any) {
			if loaderID != "" {
				handleEvent(ev)
				return
			}
			earlyEvents = append(earlyEvents, ev)
			switch ev := ev.(type) {
			case *page.EventFrameNavigated:
				// Make sure we keep frameID up to date.
				if ev.Frame.ParentID == "" {
					frameID = ev.Frame.ID
				}
			case *network.EventRequestWillBeSent:
				// Under some circumstances like ERR_TOO_MANY_REDIRECTS, we never
				// see the "init" lifecycle event we want. Those "lone" requests
				// also tend to have a loaderID that matches their requestID, for
				// some reason. If such a request is seen, use it.
				// TODO: research this some more when we have the time.
				if ev.FrameID == frameID && string(ev.LoaderID) == string(ev.RequestID) {
					loaderID = ev.LoaderID
				}
			case *page.EventLifecycleEvent:
				if ev.FrameID == frameID && ev.Name == "init" {
					loaderID = ev.LoaderID
				}
			case *page.EventNavigatedWithinDocument:
				// A fragment navigation doesn't need extra steps.
				finished = true
				lcancel()
			}
			if loaderID != "" {
				for _, ev := range earlyEvents {
					handleEvent(ev)
				}
				earlyEvents = nil
			}
		})

		// Second, run the actions.
		if err := Run(ctx, actions...); err != nil {
			return err
		}

		// Third, block until we have finished loading.
		select {
		case <-lctx.Done():
			if loadErr != nil {
				return loadErr
			}

			// If the ctx parameter was cancelled by the caller (or
			// by a timeout etc.) the select will race between
			// lctx.Done and ctx.Done, since lctx is a sub-context
			// of ctx. So we can't return nil here, as otherwise
			// that race would mean that we would drop 50% of the
			// parent context cancellation errors.
			if !finished {
				return ctx.Err()
			}
			return nil
		case <-ctx.Done():
			return ctx.Err()
		}
	})
}

// Targets lists all the targets in the browser attached to the given context.
func Targets(ctx context.Context) ([]*target.Info, error) {
	c, err := initContextBrowser(ctx)
	if err != nil {
		return nil, err
	}
	// TODO: If this is a new browser, the initial target (tab) might not be
	// ready yet. Should we block until at least one target is available?
	// Right now, the caller has to add retries with a timeout.
	return target.GetTargets().Do(cdp.WithExecutor(ctx, c.Browser))
}

// Action is the common interface for an action that will be executed against a
// context and frame handler.
type Action interface {
	// Do executes the action using the provided context and frame handler.
	Do(context.Context) error
}

// ActionFunc is an adapter to allow the use of ordinary func's as an Action.
type ActionFunc func(context.Context) error

// Do executes the func f using the provided context and frame handler.
func (f ActionFunc) Do(ctx context.Context) error {
	return f(ctx)
}

// Tasks is a sequential list of Actions that can be used as a single Action.
type Tasks []Action

// Do executes the list of Actions sequentially, using the provided context and
// frame handler.
func (t Tasks) Do(ctx context.Context) error {
	for _, a := range t {
		if err := a.Do(ctx); err != nil {
			return err
		}
	}
	return nil
}

// Sleep is an empty action that calls time.Sleep with the specified duration.
//
// Note: this is a temporary action definition for convenience, and will likely
// 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(ctx context.Context) error {
		return sleepContext(ctx, d)
	})
}

// sleepContext sleeps for the specified duration. It returns ctx.Err() immediately
// if the context is cancelled.
func sleepContext(ctx context.Context, d time.Duration) error {
	timer := time.NewTimer(d)
	select {
	case <-ctx.Done():
		if !timer.Stop() {
			<-timer.C
		}
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

// retryWithSleep reties the execution of the specified func until the func returns
// true (means to stop) or a non-nil error.
func retryWithSleep(ctx context.Context, d time.Duration, f func(ctx context.Context) (bool, error)) error {
	for {
		toStop, err := f(ctx)
		if toStop || err != nil {
			return err
		}
		err = sleepContext(ctx, d)
		if err != nil {
			return err
		}
	}
}

type cancelableListener struct {
	ctx context.Context
	fn  func(ev any)
}

// ListenBrowser adds a function which will be called whenever a browser event
// is received on the chromedp context. Note that this only includes browser
// events; command responses and target events are not included. Cancelling ctx
// stops the listener from receiving any more events.
//
// Note that the function is called synchronously when handling events. The
// function should avoid blocking at all costs. For example, any Actions must be
// run via a separate goroutine (otherwise, it could result in a deadlock if the
// action sends CDP messages).
func ListenBrowser(ctx context.Context, fn func(ev any)) {
	c := FromContext(ctx)
	if c == nil {
		panic(ErrInvalidContext)
	}
	cl := cancelableListener{ctx, fn}
	if c.Browser != nil {
		c.Browser.listenersMu.Lock()
		c.Browser.listeners = append(c.Browser.listeners, cl)
		c.Browser.listenersMu.Unlock()
	} else {
		c.browserListeners = append(c.browserListeners, cl)
	}
}

// ListenTarget adds a function which will be called whenever a target event is
// received on the chromedp context. Cancelling ctx stops the listener from
// receiving any more events.
//
// Note that the function is called synchronously when handling events. The
// function should avoid blocking at all costs. For example, any Actions must be
// run via a separate goroutine (otherwise, it could result in a deadlock if the
// action sends CDP messages).
func ListenTarget(ctx context.Context, fn func(ev any)) {
	c := FromContext(ctx)
	if c == nil {
		panic(ErrInvalidContext)
	}
	cl := cancelableListener{ctx, fn}
	if c.Target != nil {
		c.Target.listenersMu.Lock()
		c.Target.listeners = append(c.Target.listeners, cl)
		c.Target.listenersMu.Unlock()
	} else {
		c.targetListeners = append(c.targetListeners, cl)
	}
}

// WaitNewTarget can be used to wait for the current target to open a new
// target. Once fn matches a new unattached target, its target ID is sent via
// the returned channel.
func WaitNewTarget(ctx context.Context, fn func(*target.Info) bool) <-chan target.ID {
	ch := make(chan target.ID, 1)
	lctx, cancel := context.WithCancel(ctx)
	ListenTarget(lctx, func(ev any) {
		var info *target.Info
		switch ev := ev.(type) {
		case *target.EventTargetCreated:
			info = ev.TargetInfo
		case *target.EventTargetInfoChanged:
			info = ev.TargetInfo
		default:
			return
		}
		if info.OpenerID == "" {
			return // not a child target
		}
		if info.Attached {
			return // already attached; not a new target
		}
		if fn(info) {
			select {
			case <-lctx.Done():
			case ch <- info.TargetID:
			}
			close(ch)
			cancel()
		}
	})
	return ch
}


================================================
FILE: chromedp_test.go
================================================
package chromedp

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"image/color"
	"image/png"
	"io"
	"log"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path"
	"path/filepath"
	"slices"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"text/template"
	"time"

	"github.com/chromedp/cdproto/browser"
	"github.com/chromedp/cdproto/cdp"
	"github.com/chromedp/cdproto/dom"
	"github.com/chromedp/cdproto/network"
	"github.com/chromedp/cdproto/page"
	"github.com/chromedp/cdproto/runtime"
	"github.com/chromedp/cdproto/target"
	"github.com/ledongthuc/pdf"
)

var (
	// these are set up in init
	execPath    string
	testdataDir string
	allocOpts   = DefaultExecAllocatorOptions[:]

	// allocCtx is initialised in TestMain, to cancel before exiting.
	allocCtx context.Context

	// browserCtx is initialised with allocateOnce
	browserCtx context.Context
)

func init() {
	wd, err := os.Getwd()
	if err != nil {
		panic(fmt.Sprintf("could not get working directory: %v", err))
	}
	testdataDir = "file://" + path.Join(wd, "testdata")

	allocTempDir, err = os.MkdirTemp("", "chromedp-test")
	if err != nil {
		panic(fmt.Sprintf("could not create temp directory: %v", err))
	}

	// Disabling the GPU helps portability with some systems like Travis,
	// and can slightly speed up the tests on other systems.
	allocOpts = append(allocOpts, DisableGPU)

	if noHeadless := os.Getenv("CHROMEDP_NO_HEADLESS"); noHeadless != "" && noHeadless != "false" {
		allocOpts = append(allocOpts, Flag("headless", false))
	}

	// Find the exec path once at startup.
	execPath = os.Getenv("CHROMEDP_TEST_RUNNER")
	if execPath == "" {
		execPath = findExecPath()
	}
	allocOpts = append(allocOpts, ExecPath(execPath))

	// Not explicitly needed to be set, as this speeds up the tests
	if noSandbox := os.Getenv("CHROMEDP_NO_SANDBOX"); noSandbox != "false" {
		allocOpts = append(allocOpts, NoSandbox)
	}
}

var browserOpts []ContextOption

func TestMain(m *testing.M) {
	var cancel context.CancelFunc
	allocCtx, cancel = NewExecAllocator(context.Background(), allocOpts...)

	if debug := os.Getenv("CHROMEDP_DEBUG"); debug != "" && debug != "false" {
		browserOpts = append(browserOpts, WithDebugf(log.Printf))
	}

	code := m.Run()
	cancel()

	if infos, _ := os.ReadDir(allocTempDir); len(infos) > 0 {
		os.RemoveAll(allocTempDir)
		panic(fmt.Sprintf("leaked %d temporary dirs under %s", len(infos), allocTempDir))
	} else {
		os.Remove(allocTempDir)
	}

	os.Exit(code)
}

var allocateOnce sync.Once

func testAllocate(tb testing.TB, name string) (context.Context, context.CancelFunc) {
	// Start the browser exactly once, as needed.
	allocateOnce.Do(func() { browserCtx, _ = testAllocateSeparate(tb) })

	if browserCtx == nil {
		// allocateOnce.Do failed; continuing would result in panics.
		tb.FailNow()
	}

	// 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 an HTML file name, otherwise leave the blank page.
	if name != "" {
		if err := Run(ctx, Navigate(testdataDir+"/"+name)); err != nil {
			tb.Fatal(err)
		}
	}

	cancel := func() {
		if err := Cancel(ctx); err != nil {
			tb.Error(err)
		}
	}
	return ctx, cancel
}

func testAllocateSeparate(tb testing.TB) (context.Context, context.CancelFunc) {
	// Entirely new browser, unlike testAllocate.
	ctx, _ := NewContext(allocCtx, browserOpts...)
	if err := Run(ctx); err != nil {
		tb.Fatal(err)
	}
	ListenBrowser(ctx, func(ev any) {
		if ev, ok := ev.(*runtime.EventExceptionThrown); ok {
			tb.Errorf("%+v\n", ev.ExceptionDetails)
		}
	})
	cancel := func() {
		if err := Cancel(ctx); err != nil {
			tb.Error(err)
		}
	}
	return ctx, cancel
}

func BenchmarkTabNavigate(b *testing.B) {
	b.ReportAllocs()

	allocCtx, cancel := NewExecAllocator(context.Background(), allocOpts...)
	defer cancel()

	// start the browser
	bctx, _ := NewContext(allocCtx)
	if err := Run(bctx); err != nil {
		b.Fatal(err)
	}

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			ctx, _ := NewContext(bctx)
			if err := Run(ctx,
				Navigate(testdataDir+"/form.html"),
				WaitVisible(`#form`, ByID),
			); err != nil {
				b.Fatal(err)
			}
			if err := Cancel(ctx); err != nil {
				b.Fatal(err)
			}
		}
	})
}

// checkTargets fatals if the browser behind the chromedp context has an
// unexpected number of pages (tabs).
func checkTargets(tb testing.TB, ctx context.Context, want int) {
	tb.Helper()
	infos, err := Targets(ctx)
	if err != nil {
		tb.Fatal(err)
	}
	var pages []*target.Info
	for _, info := range infos {
		if info.Type == "page" {
			pages = append(pages, info)
		}
	}
	if got := len(pages); want != got {
		var summaries []string
		for _, info := range pages {
			summaries = append(summaries, fmt.Sprintf("%v", info))
		}
		tb.Fatalf("want %d targets, got %d:\n%s",
			want, got, strings.Join(summaries, "\n"))
	}
}

func TestTargets(t *testing.T) {
	t.Parallel()

	// Start one browser with one tab.
	ctx1, cancel1 := testAllocateSeparate(t)
	defer cancel1()

	checkTargets(t, 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)
	}
	checkTargets(t, ctx2, 2)

	// The first context should also see both targets.
	checkTargets(t, ctx1, 2)

	// Cancelling the second context should close the second tab alone.
	cancel2()
	checkTargets(t, 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)
	}

	// We should see one attached target, since we closed the second a while
	// ago. If we see two, that means there's a memory leak, as we're
	// holding onto the detached target.
	pages := FromContext(ctx1).Browser.pages
	if len(pages) != 1 {
		t.Fatalf("expected one attached target, got %d", len(pages))
	}
}

func TestCancelError(t *testing.T) {
	t.Parallel()

	ctx1, cancel1 := testAllocate(t, "")
	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)
	}

	if err := Cancel(allocCtx); err != ErrInvalidContext {
		t.Fatalf("want error %q, got %q", ErrInvalidContext, err)
	}

	/*
		// NOTE: the following test no longer is applicable, as a slight change
		// to chromium's 89 API deprecated the boolean return value

		// 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, _ := NewContext(allocCtx, browserOpts...)
	if err := Cancel(ctx); err != nil {
		t.Fatal(err)
	}
	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 := testAllocate(t, "")
	defer cancel()
	if err := Run(ctx1); err != nil {
		t.Fatal(err)
	}

	ctx2, cancel := NewContext(ctx1)
	// Cancel after the browser is allocated, but before we've created a new
	// tab.
	cancel()
	if err := Run(ctx2); err != context.Canceled {
		t.Fatalf("wanted canceled context error, got %v", err)
	}
}

func TestPrematureCancelAllocator(t *testing.T) {
	t.Parallel()

	// To ensure we don't actually fire any Chrome processes.
	allocCtx, cancel := NewExecAllocator(context.Background(),
		ExecPath("/do-not-run-chrome"))
	// Cancel before the browser is allocated.
	cancel()

	ctx, cancel := NewContext(allocCtx)
	defer cancel()
	if err := Run(ctx); err != context.Canceled {
		t.Fatalf("wanted canceled context error, got %v", err)
	}
}

func TestConcurrentCancel(t *testing.T) {
	t.Parallel()

	// To ensure we don't actually fire any Chrome processes.
	allocCtx, cancel := NewExecAllocator(context.Background(),
		ExecPath("/do-not-run-chrome"))
	defer cancel()

	var wg sync.WaitGroup
	// 50 is enough for 'go test -race' to easily spot issues.
	for range 50 {
		wg.Add(2)
		ctx, cancel := NewContext(allocCtx)
		go func() {
			cancel()
			wg.Done()
		}()
		go func() {
			_ = Run(ctx)
			wg.Done()
		}()
	}
	wg.Wait()
}

func TestListenBrowser(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	// Check that many ListenBrowser callbacks work, including adding
	// callbacks after the browser has been allocated.
	var totalCount int32
	ListenBrowser(ctx, func(ev any) {
		// using sync/atomic, as the browser is shared.
		atomic.AddInt32(&totalCount, 1)
	})
	if err := Run(ctx); err != nil {
		t.Fatal(err)
	}
	seenSessions := make(map[target.SessionID]bool)
	ListenBrowser(ctx, func(ev any) {
		if ev, ok := ev.(*target.EventAttachedToTarget); ok {
			seenSessions[ev.SessionID] = true
		}
	})

	newTabCtx, cancel := NewContext(ctx)
	defer cancel()
	if err := Run(newTabCtx, Navigate(testdataDir+"/form.html")); err != nil {
		t.Fatal(err)
	}
	cancel()
	if id := FromContext(newTabCtx).Target.SessionID; !seenSessions[id] {
		t.Fatalf("did not see Target.attachedToTarget for %q", id)
	}
	if want, got := int32(1), atomic.LoadInt32(&totalCount); got < want {
		t.Fatalf("want at least %d browser events; got %d", want, got)
	}
}

func TestListenTarget(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	// Check that many listen callbacks work, including adding callbacks
	// after the target has been attached to.
	var navigatedCount, updatedCount int
	ListenTarget(ctx, func(ev any) {
		if _, ok := ev.(*page.EventFrameNavigated); ok {
			navigatedCount++
		}
	})
	if err := Run(ctx); err != nil {
		t.Fatal(err)
	}
	ListenTarget(ctx, func(ev any) {
		if _, ok := ev.(*dom.EventDocumentUpdated); ok {
			updatedCount++
		}
	})

	if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil {
		t.Fatal(err)
	}
	cancel()
	if want := 1; navigatedCount != want {
		t.Fatalf("want %d Page.frameNavigated events; got %d", want, navigatedCount)
	}
	if want := 1; updatedCount < want {
		t.Fatalf("want at least %d DOM.documentUpdated events; got %d", want, updatedCount)
	}
}

func TestLargeEventCount(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	// Simulate an environment where Chrome sends 2000 console log events,
	// and we are slow at processing them. In older chromedp versions, this
	// would crash as we would fill eventQueue and panic. 50ms is enough to
	// make the test fail somewhat reliably on old chromedp versions,
	// without making the test too slow.
	first := true
	ListenTarget(ctx, func(ev any) {
		if _, ok := ev.(*runtime.EventConsoleAPICalled); ok && first {
			time.Sleep(50 * time.Millisecond)
			first = false
		}
	})

	if err := Run(ctx,
		Navigate(testdataDir+"/consolespam.html"),
		WaitVisible("#done", ByID), // wait for the JS to finish
	); err != nil {
		t.Fatal(err)
	}
}

func TestLargeQuery(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "<html><body>\n")
		for i := range 2000 {
			fmt.Fprintf(w, `<div>`)
			fmt.Fprintf(w, `<a href="/%d">link %d</a>`, i, i)
			fmt.Fprintf(w, `</div>`)
		}
		fmt.Fprintf(w, "</body></html>\n")
	}))
	defer s.Close()

	// ByQueryAll queries thousands of events, which triggers thousands of
	// DOM events. The target handler used to get into a deadlock, as the
	// event queues would fill up and prevent the wait function from
	// receiving any result.
	var nodes []*cdp.Node
	if err := Run(ctx,
		Navigate(s.URL),
		Nodes("a", &nodes, ByQueryAll),
	); err != nil {
		t.Fatal(err)
	}
}

func TestDialTimeout(t *testing.T) {
	t.Parallel()

	t.Run("ShortTimeoutError", func(t *testing.T) {
		t.Parallel()
		l, err := net.Listen("tcp", ":0")
		if err != nil {
			t.Fatal(err)
		}
		url := "ws://" + l.(*net.TCPListener).Addr().String()
		defer l.Close()

		ctx := t.Context()
		_, err = NewBrowser(ctx, url, WithDialTimeout(time.Microsecond))
		got, want := fmt.Sprintf("%v", err), "i/o timeout"
		if !strings.Contains(got, want) {
			t.Fatalf("got %q, want %q", got, want)
		}
	})
	t.Run("NoTimeoutSuccess", func(t *testing.T) {
		t.Parallel()
		l, err := net.Listen("tcp", ":0")
		if err != nil {
			t.Fatal(err)
		}
		url := "ws://" + l.(*net.TCPListener).Addr().String()
		defer l.Close()
		go func() {
			conn, err := l.Accept()
			if err == nil {
				conn.Close()
			}
		}()

		ctx := t.Context()
		_, err = NewBrowser(ctx, url, WithDialTimeout(0))
		got := fmt.Sprintf("%v", err)
		if !strings.Contains(got, "EOF") && !strings.Contains(got, "connection reset") {
			t.Fatalf("got %q, want %q or %q", got, "EOF", "connection reset")
		}
	})
}

func TestListenCancel(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocateSeparate(t)
	defer cancel()

	// Check that cancelling a listen context stops the listener.
	var browserCount, targetCount int

	ctx1, cancel1 := context.WithCancel(ctx)
	ListenBrowser(ctx1, func(ev any) {
		browserCount++
		cancel1()
	})

	ctx2, cancel2 := context.WithCancel(ctx)
	ListenTarget(ctx2, func(ev any) {
		targetCount++
		cancel2()
	})

	if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil {
		t.Fatal(err)
	}
	if want := 1; browserCount != 1 {
		t.Fatalf("want %d browser events; got %d", want, browserCount)
	}
	if want := 1; targetCount != 1 {
		t.Fatalf("want %d target events; got %d", want, targetCount)
	}
}

func TestLogOptions(t *testing.T) {
	t.Parallel()

	var bufMu sync.Mutex
	var buf bytes.Buffer
	fn := func(format string, a ...any) {
		bufMu.Lock()
		fmt.Fprintf(&buf, format, a...)
		fmt.Fprintln(&buf)
		bufMu.Unlock()
	}

	ctx, cancel := NewContext(context.Background(),
		WithErrorf(fn),
		WithLogf(fn),
		WithDebugf(fn),
	)
	defer cancel()
	if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil {
		t.Fatal(err)
	}
	cancel()

	bufMu.Lock()
	got := buf.String()
	bufMu.Unlock()
	for _, want := range []string{
		"Page.navigate",
		"Page.frameNavigated",
	} {
		if !strings.Contains(got, want) {
			t.Errorf("expected output to contain %q", want)
		}
	}
}

func TestBrowserContext(t *testing.T) {
	ctx, cancel := testAllocate(t, "child1.html")
	defer cancel()
	// There is not a dedicated cdp command to get the default browser context.
	// Our workaround is to get it from a target which is created without the
	// "browserContextId" parameter.
	defaultBrowserContextID := getBrowserContext(t, ctx)

	// Prepare 2 browser contexts to be used later.
	rootCtx1, cancel := NewContext(browserCtx, WithNewBrowserContext())
	defer cancel()
	if err := Run(rootCtx1); err != nil {
		t.Fatal(err)
	}
	rootBrowserContextID1 := FromContext(rootCtx1).BrowserContextID

	rootCtx2, cancel := NewContext(browserCtx, WithNewBrowserContext())
	defer cancel()
	if err := Run(rootCtx2); err != nil {
		t.Fatal(err)
	}
	rootBrowserContextID2 := FromContext(rootCtx2).BrowserContextID

	tests := []struct {
		name         string
		arrange      func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID)
		wantDisposed bool
		wantPanic    string
	}{
		{
			name: "default",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, cancel := NewContext(browserCtx)
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				return ctx, cancel, defaultBrowserContextID
			},
			wantDisposed: false,
			wantPanic:    "",
		},
		{
			name: "new",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, cancel := NewContext(browserCtx, WithNewBrowserContext())
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				c := FromContext(ctx)
				return ctx, cancel, c.BrowserContextID
			},
			wantDisposed: true,
			wantPanic:    "",
		},
		{
			name: "existing",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, cancel := NewContext(browserCtx, WithExistingBrowserContext(rootBrowserContextID1))
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				return ctx, cancel, rootBrowserContextID1
			},
			wantDisposed: false,
			wantPanic:    "",
		},
		{
			name: "inherited 1",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, cancel := NewContext(rootCtx1)
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				return ctx, cancel, rootBrowserContextID1
			},
			wantDisposed: false,
			wantPanic:    "",
		},
		{
			name: "inherited 2",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx1, _ := NewContext(rootCtx1)
				if err := Run(ctx1); err != nil {
					t.Fatal(err)
				}
				ctx, cancel := NewContext(ctx1)
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				return ctx, cancel, rootBrowserContextID1
			},
			wantDisposed: false,
			wantPanic:    "",
		},
		{
			name: "inherited 3",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx1, _ := NewContext(browserCtx, WithExistingBrowserContext(rootBrowserContextID1))
				if err := Run(ctx1); err != nil {
					t.Fatal(err)
				}
				ctx, cancel := NewContext(ctx1)
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				return ctx, cancel, rootBrowserContextID1
			},
			wantDisposed: false,
			wantPanic:    "",
		},
		{
			name: "break inheritance 1",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, cancel := NewContext(rootCtx1, WithExistingBrowserContext(rootBrowserContextID2))
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				// The target should be added to the second browser context.
				return ctx, cancel, rootBrowserContextID2
			},
			wantDisposed: false,
			wantPanic:    "",
		},
		{
			name: "break inheritance 2",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, cancel := NewContext(rootCtx1, WithNewBrowserContext())
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				c := FromContext(ctx)
				if c.BrowserContextID == rootBrowserContextID1 {
					t.Fatal("a new BrowserContext should be created")
				}
				return ctx, cancel, c.BrowserContextID
			},
			wantDisposed: true,
			wantPanic:    "",
		},
		{
			name: "break inheritance 3",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, cancel := NewContext(rootCtx1, WithTargetID(FromContext(rootCtx2).Target.TargetID))
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}

				c := FromContext(ctx)
				if c.BrowserContextID != "" {
					t.Fatal("when a context is used to attach to a tab, its BrowserContextID should be empty")
				}

				return ctx, cancel, rootBrowserContextID2
			},
			wantDisposed: false,
			wantPanic:    "",
		},
		{
			name: "WithNewBrowserContext when WithTargetID is specified",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, _ := NewContext(rootCtx1)
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				ctx, cancel := NewContext(browserCtx, WithTargetID(FromContext(ctx).Target.TargetID), WithNewBrowserContext())
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}

				return ctx, cancel, rootBrowserContextID1
			},
			wantDisposed: false,
			wantPanic:    "WithNewBrowserContext can not be used when WithTargetID is specified",
		},
		{
			name: "WithExistingBrowserContext when WithTargetID is specified",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, _ := NewContext(rootCtx1)
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}
				ctx, cancel := NewContext(browserCtx, WithTargetID(FromContext(ctx).Target.TargetID), WithExistingBrowserContext(rootBrowserContextID2))
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}

				return ctx, cancel, rootBrowserContextID1
			},
			wantDisposed: false,
			wantPanic:    "WithExistingBrowserContext can not be used when WithTargetID is specified",
		},
		{
			name: "WithNewBrowserContext before Browser is initialized",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, cancel := NewContext(context.Background(), WithNewBrowserContext())
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}

				return ctx, cancel, ""
			},
			wantDisposed: false,
			wantPanic:    "WithNewBrowserContext can not be used before Browser is initialized",
		},
		{
			name: "WithExistingBrowserContext before Browser is initialized",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				ctx, cancel := NewContext(context.Background(), WithExistingBrowserContext(rootBrowserContextID1))
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}

				return ctx, cancel, ""
			},
			wantDisposed: false,
			wantPanic:    "WithExistingBrowserContext can not be used before Browser is initialized",
		},
		{
			name: "remote allocator WithExistingBrowserContext ",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				c := FromContext(browserCtx)
				var conn *net.TCPConn
				if chromedpConn, ok := c.Browser.conn.(*Conn); ok {
					conn, _ = chromedpConn.conn.(*net.TCPConn)
				}
				if conn == nil {
					t.Skip("skip when the remote debugging address is not available")
				}
				actx, _ := NewRemoteAllocator(context.Background(), "ws://"+conn.RemoteAddr().String())
				ctx, cancel := NewContext(actx, WithExistingBrowserContext(rootBrowserContextID1))
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}

				return ctx, cancel, rootBrowserContextID1
			},
			wantDisposed: false,
			wantPanic:    "",
		},
		{
			name: "remote allocator WithNewBrowserContext",
			arrange: func(t *testing.T) (context.Context, context.CancelFunc, cdp.BrowserContextID) {
				c := FromContext(browserCtx)
				var conn *net.TCPConn
				if chromedpConn, ok := c.Browser.conn.(*Conn); ok {
					conn, _ = chromedpConn.conn.(*net.TCPConn)
				}
				if conn == nil {
					t.Skip("skip when the remote debugging address is not available")
				}
				actx, _ := NewRemoteAllocator(context.Background(), "ws://"+conn.RemoteAddr().String())
				ctx, cancel := NewContext(actx, WithNewBrowserContext())
				if err := Run(ctx); err != nil {
					t.Fatal(err)
				}

				return ctx, cancel, FromContext(ctx).BrowserContextID
			},
			wantDisposed: true,
			wantPanic:    "",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if test.wantPanic != "" {
				defer func() {
					if got := fmt.Sprint(recover()); got != test.wantPanic {
						t.Errorf("want panic %q, got %q", test.wantPanic, got)
					}
				}()
			}
			ctx, cancel, want := test.arrange(t)
			defer cancel()

			got := getBrowserContext(t, ctx)

			if got != want {
				switch want {
				case defaultBrowserContextID:
					t.Errorf("want default browser context %q, got %q", want, got)
				case rootBrowserContextID1:
					t.Errorf("want root browser context 1 %q, got %q", want, got)
				case rootBrowserContextID2:
					t.Errorf("want root browser context 2 %q, got %q", want, got)
				default:
					t.Errorf("want browser context %q, got %q", want, got)
				}
			}

			if want == defaultBrowserContextID {
				// There is not way to check whether the default browser context
				// is disposed, so stop here.
				return
			}

			cancel()

			var ids []cdp.BrowserContextID
			if err := Run(browserCtx,
				ActionFunc(func(ctx context.Context) error {
					c := FromContext(ctx)
					var err error
					ids, err = target.GetBrowserContexts().Do(cdp.WithExecutor(ctx, c.Browser))
					return err
				}),
			); err != nil {
				t.Fatal(err)
			}

			disposed := !slices.Contains(ids, want)

			if disposed != test.wantDisposed {
				t.Errorf("browser context disposed = %v, want %v", disposed, test.wantDisposed)
			}
		})
	}
}

func getBrowserContext(tb testing.TB, ctx context.Context) cdp.BrowserContextID {
	var id cdp.BrowserContextID
	if err := Run(ctx,
		ActionFunc(func(ctx context.Context) error {
			info, err := target.GetTargetInfo().Do(ctx)
			id = info.BrowserContextID
			return err
		}),
	); err != nil {
		tb.Fatal(err)
	}
	return id
}

func TestLargeOutboundMessages(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	// ~5MiB of JS to test the grow feature of github.com/gobwas/ws.
	expr := fmt.Sprintf("//%s\n", strings.Repeat("x", 5<<20))
	res := new([]byte)
	if err := Run(ctx, Evaluate(expr, res)); err != nil {
		t.Fatal(err)
	}
}

func TestDirectCloseTarget(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	c := FromContext(ctx)
	want := "to close the target, cancel its context"

	// Check that nothing is closed by running the action twice.
	for range 2 {
		err := Run(ctx, ActionFunc(func(ctx context.Context) error {
			return target.CloseTarget(c.Target.TargetID).Do(ctx)
		}))
		got := fmt.Sprint(err)
		if !strings.Contains(got, want) {
			t.Fatalf("want %q, got %q", want, got)
		}
	}
}

func TestDirectCloseBrowser(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocateSeparate(t)
	defer cancel()

	c := FromContext(ctx)
	want := "use chromedp.Cancel"

	// Check that nothing is closed by running the action twice.
	for range 2 {
		err := browser.Close().Do(cdp.WithExecutor(ctx, c.Browser))
		got := fmt.Sprint(err)
		if !strings.Contains(got, want) {
			t.Fatalf("want %q, got %q", want, got)
		}
	}
}

func TestDownloadIntoDir(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	dir := t.TempDir()

	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/data.bin":
			w.Header().Set("Content-Type", "application/octet-stream")
			fmt.Fprintf(w, "some binary data")
		default:
			w.Header().Set("Content-Type", "text/html")
			fmt.Fprintf(w, `go <a id="download" href="/data.bin">download</a> stuff/`)
		}
	}))
	defer s.Close()

	done := make(chan string, 1)
	ListenTarget(ctx, func(v any) {
		if ev, ok := v.(*browser.EventDownloadProgress); ok {
			if ev.State == browser.DownloadProgressStateCompleted {
				done <- ev.GUID
				close(done)
			}
		}
	})

	if err := Run(ctx,
		Navigate(s.URL),
		browser.SetDownloadBehavior(browser.SetDownloadBehaviorBehaviorAllowAndName).WithDownloadPath(dir).WithEventsEnabled(true),
		Click("#download", ByQuery),
	); err != nil {
		t.Fatal(err)
	}

	select {
	case <-ctx.Done():
		t.Fatalf("unexpected error: %v", ctx.Err())
	case guid := <-done:
		if _, err := os.Stat(filepath.Join(dir, guid)); err != nil {
			t.Fatalf("want error nil, got: %v", err)
		}
	}
}

func TestGracefulBrowserShutdown(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()

	// TODO(mvdan): this doesn't work with DefaultExecAllocatorOptions+UserDataDir
	opts := []ExecAllocatorOption{
		NoFirstRun,
		NoDefaultBrowserCheck,
		Headless,
		UserDataDir(dir),
	}
	actx, cancel := NewExecAllocator(context.Background(), opts...)
	defer cancel()

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.RequestURI == "/set" {
			http.SetCookie(w, &http.Cookie{
				Name:    "cookie1",
				Value:   "value1",
				Expires: time.Now().AddDate(0, 0, 1), // one day later
			})
		}
	}))
	defer ts.Close()

	{
		ctx, _ := NewContext(actx)
		if err := Run(ctx, Navigate(ts.URL+"/set")); err != nil {
			t.Fatal(err)
		}

		// Close the browser gracefully.
		if err := Cancel(ctx); err != nil {
			t.Fatal(err)
		}
	}
	{
		ctx, _ := NewContext(actx)
		var got string
		if err := Run(ctx,
			Navigate(ts.URL),
			EvaluateAsDevTools("document.cookie", &got),
		); err != nil {
			t.Fatal(err)
		}
		if want := "cookie1=value1"; got != want {
			t.Fatalf("want cookies %q; got %q", want, got)
		}
	}
}

func TestAttachingToWorkers(t *testing.T) {
	t.Parallel()

	for _, tc := range []struct {
		desc, pageJS, wantSelf string
	}{
		{"DedicatedWorker", "new Worker('/worker.js')", "DedicatedWorkerGlobalScope"},
		{"ServiceWorker", "navigator.serviceWorker.register('/worker.js')", "ServiceWorkerGlobalScope"},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			mux := http.NewServeMux()
			mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
				w.Header().Set("Content-Type", "text/html")
				fmt.Fprintf(w, `
					<html>
						<body>
							<script>
								%s
							</script>
						</body>
					</html>`, tc.pageJS)
			})
			mux.HandleFunc("/worker.js", func(w http.ResponseWriter, r *http.Request) {
				w.Header().Set("Content-Type", "text/javascript")
				io.WriteString(w, "console.log('I am worker code.');")
			})
			ts := httptest.NewServer(mux)
			defer ts.Close()

			ctx, cancel := NewContext(context.Background())
			defer cancel()

			ch := make(chan target.ID, 1)

			ListenTarget(ctx, func(ev any) {
				if ev, ok := ev.(*target.EventAttachedToTarget); ok {
					if !strings.Contains(ev.TargetInfo.Type, "worker") {
						return
					}
					ch <- ev.TargetInfo.TargetID
				}
			})

			if err := Run(ctx, Navigate(ts.URL)); err != nil {
				t.Fatalf("Failed to navigate to the test page: %q", err)
			}

			targetID := <-ch
			ctx, cancel = NewContext(ctx, WithTargetID(targetID))
			defer cancel()

			if err := Run(ctx, ActionFunc(func(ctx context.Context) error {
				if r, _, err := runtime.Evaluate("self").Do(ctx); err != nil {
					return err
				} else if r.ClassName != tc.wantSelf {
					return fmt.Errorf("Global scope type mismatch: got %q want: %q", r.ClassName, tc.wantSelf)
				}
				return nil
			})); err != nil {
				t.Fatalf("Failed to check evaluating JavaScript in a worker target: %q", err)
			}
		})
	}
}

func TestRunResponse(t *testing.T) {
	t.Parallel()

	// This test includes many edge cases for RunResponse; navigations that
	// fail to start, responses that return errors, responses that redirect,
	// and so on.
	// We also test each of those with different actions, such as a straight
	// navigation, as well as a click.
	// What's important here is that we have an iframe that keeps reloading
	// every 100ms in the main page. If RunResponse doesn't properly filter
	// events for the top level frame, the tests should fail pretty often.

	indexTmpl := template.Must(template.New("").Parse(`
		<html>
			<body>
				<a id="url_index" href="/index">index</a>
				<a id="url_200" href="/200">200</a>
				<a id="url_404" href="/404">404</a>
				<a id="url_500" href="/500">500</a>
				<a id="url_badtls" href="https://{{.Host}}/index">badtls</a>
				<a id="url_badprotocol" href="bad://{{.Host}}/index">badprotocol</a>
				<a id="url_unimplementedprotocol" href="ftp://{{.Host}}/index">unimplementedprotocol</a>
				<a id="url_plain" href="/plain">plain</a>
				<a id="url_two" href="/two">two</a>
				<a id="url_one" href="/one">one</a>
				<a id="url_infinite" href="/infinite">infinite</a>
				<a id="url_badiframe" href="/badiframe">badiframe</a>

				<script>
					setInterval(function(){
						document.getElementById("reloadingframe").src += "";
					}, 100);
				</script>
				<iframe id="reloadingframe" src="/reloadingframe"></iframe>
			</body>
		</html>`))
	mux := http.NewServeMux()
	mux.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")
		indexTmpl.Execute(w, r)
	})
	mux.HandleFunc("/200", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")
		fmt.Fprintf(w, "OK")
	})
	mux.HandleFunc("/500", func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "500", 500)
	})
	mux.HandleFunc("/plain", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain")
		fmt.Fprintf(w, "OK")
	})
	mux.HandleFunc("/two", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/one", http.StatusMovedPermanently)
	})
	mux.HandleFunc("/one", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/zero", http.StatusMovedPermanently)
	})
	mux.HandleFunc("/zero", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "OK")
	})
	mux.HandleFunc("/infinite", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/infinite", http.StatusMovedPermanently)
	})
	mux.HandleFunc("/badiframe", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")
		fmt.Fprintf(w, `<html><body><iframe src="badurl://localhost/"></iframe></body></html>`)
	})
	ts := httptest.NewServer(mux)
	t.Cleanup(ts.Close)

	tests := []struct {
		name string
		url  string

		wantErr       string
		wantSuffixURL string
		wantStatus    int64
	}{
		{
			name:       "200",
			url:        "200",
			wantStatus: 200,
		},
		{
			name:       "404",
			url:        "404",
			wantStatus: 404,
		},
		{
			name:       "500",
			url:        "500",
			wantStatus: 500,
		},

		// Use the local http server as https, which should be a TLS
		// error and fail to load. If we don't capture the "loading
		// failed" error, we will block until the timeout is hit and
		// give a generic "deadline exceeded" error.
		{
			name:    "BadTLS",
			url:     strings.ReplaceAll(ts.URL, "http://", "https://") + "/index",
			wantErr: "ERR_SSL_PROTOCOL_ERROR",
		},

		// In this case, the "loading failed" event is received, but the
		// load itself is cancelled immediately, so we never receive a
		// load event of any sort.
		{
			name:    "BadProtocol",
			url:     strings.ReplaceAll(ts.URL, "http://", "bad://") + "/index",
			wantErr: "ERR_ABORTED",
		},

		// Check that loading a non-HTML document still works normally.
		{
			name:          "NonHTML",
			url:           "plain",
			wantSuffixURL: "/plain",
		},

		{
			name:          "BadIframe",
			url:           "badiframe",
			wantSuffixURL: "/badiframe",
		},

		{
			name:          "OneRedirect",
			url:           "one",
			wantSuffixURL: "/zero",
		},
		{
			name:          "TwoRedirects",
			url:           "two",
			wantSuffixURL: "/zero",
		},
		{
			name:    "InfiniteRedirects",
			url:     "infinite",
			wantErr: "ERR_TOO_MANY_REDIRECTS",
		},
	}

	for _, test := range tests {
		allocate := func(t *testing.T) context.Context {
			ctx, cancel := testAllocate(t, "")
			t.Cleanup(cancel)
			ctx, cancel = context.WithTimeout(ctx, 5*time.Second)
			t.Cleanup(cancel)

			if err := Run(ctx, Navigate(ts.URL+"/index")); err != nil {
				t.Fatalf("Failed to navigate to the test page: %q", err)
			}
			return ctx
		}
		checkResults := func(t *testing.T, resp *network.Response, err error) {
			if test.wantErr == "" && err != nil {
				t.Fatalf("wanted nil error, got %v", err)
			}
			if got := fmt.Sprint(err); !strings.Contains(got, test.wantErr) {
				t.Fatalf("wanted error to contain %q, got %q", test.wantErr, got)
			}
			if test.wantErr == "" && resp == nil {
				t.Fatalf("expected response to be non-nil")
			} else if test.wantErr != "" && resp != nil {
				t.Fatalf("expected response to be nil")
			}

			url := ""
			status := int64(0)
			if resp != nil {
				url = resp.URL
				status = resp.Status
			}
			if !strings.HasSuffix(url, test.wantSuffixURL) {
				t.Fatalf("wanted response URL to end with %q, got %q", test.wantSuffixURL, url)
			}
			if want := test.wantStatus; want != 0 && status != want {
				t.Fatalf("wanted status code %d, got %d", want, status)
			}

			if resp != nil {
				latency := time.Since(resp.ResponseTime.Time())
				if latency > time.Hour || latency < -time.Hour {
					t.Errorf("responseTime does not hold a reasonable value %s. "+
						"Maybe it's in seconds now and we should remove the workaround. "+
						"See https://github.com/chromedp/pdlgen/issues/22.",
						resp.ResponseTime.Time())
				}
			}
		}
		t.Run("Navigate"+test.name, func(t *testing.T) {
			t.Parallel()
			ctx := allocate(t)

			url := test.url
			if !strings.Contains(url, "/") {
				url = ts.URL + "/" + url
			}
			resp, err := RunResponse(ctx, Navigate(url))
			checkResults(t, resp, err)
		})
		t.Run("Click"+test.name, func(t *testing.T) {
			t.Parallel()
			ctx := allocate(t)

			query := "#url_" + strings.ToLower(test.name)
			if !strings.Contains(test.url, "/") {
				query = "#url_" + test.url
			}
			resp, err := RunResponse(ctx, Click(query, ByQuery))
			checkResults(t, resp, err)
		})
	}
}

func TestRunResponse_noResponse(t *testing.T) {
	t.Parallel()

	mux := http.NewServeMux()
	mux.HandleFunc("/200", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")
		fmt.Fprintf(w, `<html><body>
		<a id="same" href="/200">same</a>
		<a id="fragment" href="/200#fragment">fragment</a>
		</body></html>`)
	})
	ts := httptest.NewServer(mux)
	defer ts.Close()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	ctx, cancel = context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	steps := []struct {
		name     string
		action   Action
		wantResp bool
	}{
		{"FirstNavigation", Navigate(ts.URL + "/200"), true},
		{"RepeatedNavigation", Navigate(ts.URL + "/200"), true},
		{"FragmentNavigation", Navigate(ts.URL + "/200#foo"), false},

		{"FirstClick", Click("#same", ByQuery), true},
		{"RepeatedClick", Click("#same", ByQuery), true},
		{"FragmentClick", Click("#fragment", ByQuery), false},

		{"Blank", Navigate("about:blank"), false},
	}
	// Don't use sub-tests, as these are all sequential steps that can't
	// happen independently of each other.
	for _, step := range steps {
		resp, err := RunResponse(ctx, step.action)
		if err != nil {
			t.Fatalf("%s: %v", step.name, err)
		}
		if resp == nil && step.wantResp {
			t.Fatalf("%s: wanted a response, got nil", step.name)
		} else if resp != nil && !step.wantResp {
			t.Fatalf("%s: did not want a response, got: %#v", step.name, resp)
		}
	}
}

// TestWebGL tests that WebGL is correctly configured in headless-shell.
//
// This is a regress test for https://github.com/chromedp/chromedp/issues/1073.
func TestWebGL(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "webgl.html")
	defer cancel()

	var buf []byte
	if err := Run(ctx,
		Poll("rendered", nil, WithPollingTimeout(2*time.Second)),
		Screenshot(`#c`, &buf, ByQuery),
	); err != nil {
		if errors.Is(err, ErrPollingTimeout) {
			t.Fatal("The cube is not rendered in 2s.")
		} else {
			t.Fatal(err)
		}
	}

	img, err := png.Decode(bytes.NewReader(buf))
	if err != nil {
		t.Fatal(err)
	}

	bounds := img.Bounds()
	if bounds.Dx() != 200 || bounds.Dy() != 200 {
		t.Fatalf("Unexpected screenshot size. got: %d x %d, want 200 x 200.", bounds.Dx(), bounds.Dy())
	}

	isWhite := func(c color.Color) bool {
		r, g, b, _ := c.RGBA()
		return r == 0xffff && g == 0xffff && b == 0xffff
	}
	if isWhite(img.At(100, 100)) {
		t.Fatal("When the cube is rendered correctly, the color at the middle of the canvas should not be white.")
	}
}

// TestPDFTemplate tests that the resource pack is loaded in headless-shell.
//
// When it's correctly loaded, the header/footer templates that use the
// following values should work as expected:
//   - title
//   - url
//   - pageNumber
//   - totalPages
//
// This is a regress test for https://github.com/chromedp/chromedp/issues/922.
func TestPDFTemplate(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	var buf []byte
	if err := Run(ctx,
		Navigate("about:blank"),
		ActionFunc(func(ctx context.Context) error {
			frameTree, err := page.GetFrameTree().Do(ctx)
			if err != nil {
				return err
			}

			return page.SetDocumentContent(frameTree.Frame.ID, `
				<html>
					<head>
						<title>PDF Template</title>
					</head>
					<body>
						Hello World!
					</body>
				</html>
			`).Do(ctx)
		}),
		ActionFunc(func(ctx context.Context) error {
			var err error
			buf, _, err = page.PrintToPDF().
				WithMarginTop(0.5).
				WithMarginBottom(0.5).
				WithDisplayHeaderFooter(true).
				WithHeaderTemplate(`<div style="font-size:8px;width:100%;text-align:center;"><span class="title"></span> -- <span class="url"></span></div>`).
				WithFooterTemplate(`<div style="font-size:8px;width:100%;text-align:center;">(<span class="pageNumber"></span> / <span class="totalPages"></span>)</div>`).
				Do(ctx)

			return err
		}),
	); err != nil {
		t.Fatal(err)
	}

	r, err := pdf.NewReader(bytes.NewReader(buf), int64(len(buf)))
	if err != nil {
		t.Fatal(err)
	}
	b, err := r.GetPlainText()
	if err != nil {
		t.Fatal(err)
	}

	want := []byte("Hello World!PDF Template -- about:blank(1 / 1)")
	l := len(want)
	// try to reuse buf
	if len(buf) >= l {
		buf = buf[0:l]
	} else {
		buf = make([]byte, l)
	}
	n, err := io.ReadFull(b, buf)
	if err != nil && !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) {
		t.Fatal(err)
	}
	buf = buf[:n]

	if !bytes.Equal(buf, want) {
		t.Errorf("page.PrintToPDF produces unexpected content. got: %q, want: %q", buf, want)
	}
}

// regression test for https://github.com/chromedp/chromedp/issues/1551
func TestPDFBackground(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "")
	defer cancel()

	var buf []byte
	if err := Run(ctx,
		Navigate("about:blank"),
		ActionFunc(func(ctx context.Context) error {
			frameTree, err := page.GetFrameTree().Do(ctx)
			if err != nil {
				return err
			}
			return page.SetDocumentContent(frameTree.Frame.ID, `
				<html lang="en">
					<head></head>
					<body style="background-color:green">
						<p>Lorem ipsum</p>
					</body>
				</html>
			`).Do(ctx)
		}),
		ActionFunc(func(ctx context.Context) error {
			var err error
			buf, _, err = page.PrintToPDF().Do(ctx)
			return err
		}),
	); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile("background.pdf", buf, 0o644); err != nil {
		t.Fatal(err)
	}
}


================================================
FILE: conn.go
================================================
package chromedp

import (
	"bytes"
	"context"
	"io"
	"net"

	"github.com/chromedp/cdproto"
	jsonv2 "github.com/go-json-experiment/json"
	"github.com/go-json-experiment/json/jsontext"
	"github.com/gobwas/ws"
	"github.com/gobwas/ws/wsutil"
)

// Transport is the common interface to send/receive messages to a target.
//
// This interface is currently used internally by Browser, but it is exposed as
// it will be useful as part of the public API in the future.
type Transport interface {
	Read(context.Context, *cdproto.Message) error
	Write(context.Context, *cdproto.Message) error
	io.Closer
}

// Conn implements Transport with a gobwas/ws websocket connection.
type Conn struct {
	conn net.Conn

	// reuse the websocket reader and writer to avoid an alloc per
	// Read/Write.
	reader wsutil.Reader
	writer wsutil.Writer

	// reuse the easyjson structs to avoid allocs per Read/Write.
	decoder jsontext.Decoder
	encoder jsontext.Encoder

	debugf func(string, ...any)
}

// DialContext dials the specified websocket URL using gobwas/ws.
func DialContext(ctx context.Context, urlstr string, opts ...DialOption) (*Conn, error) {
	// connect
	conn, br, _, err := ws.Dial(ctx, urlstr)
	if err != nil {
		return nil, err
	}
	if br != nil {
		panic("br should be nil")
	}

	// apply opts
	c := &Conn{
		conn: conn,
		// pass 0 to use the default initial buffer size (4KiB).
		// github.com/gobwas/ws will grow the buffer size if needed.
		writer: *wsutil.NewWriterBufferSize(conn, ws.StateClientSide, ws.OpText, 0),
	}
	for _, o := range opts {
		o(c)
	}

	return c, nil
}

// Close satisfies the io.Closer interface.
func (c *Conn) Close() error {
	return c.conn.Close()
}

// Read reads the next message.
func (c *Conn) Read(_ context.Context, msg *cdproto.Message) error {
	// get websocket reader
	c.reader = wsutil.Reader{Source: c.conn, State: ws.StateClientSide}
	h, err := c.reader.NextFrame()
	if err != nil {
		return err
	}

	if h.OpCode == ws.OpPing { // ping
		if c.debugf != nil {
			c.debugf("received ping frame, ignoring...")
		}
		return nil
	} else if h.OpCode == ws.OpClose { // close
		if c.debugf != nil {
			c.debugf("received close frame")
		}
		return io.EOF
	} else if h.OpCode != ws.OpText {
		if c.debugf != nil {
			c.debugf("unknown OpCode: %s", h.OpCode)
		}
		return ErrInvalidWebsocketMessage
	}

	var b bytes.Buffer
	if _, err := b.ReadFrom(&c.reader); err != nil {
		return err
	}

	if c.debugf != nil {
		c.debugf("<- %s", b.Bytes())
	}

	// unmarshal, reusing decoder
	c.decoder.Reset(&b, DefaultUnmarshalOptions)
	return jsonv2.UnmarshalDecode(&c.decoder, msg, DefaultUnmarshalOptions)
}

// Write writes a message.
func (c *Conn) Write(_ context.Context, msg *cdproto.Message) error {
	c.writer.Reset(c.conn, ws.StateClientSide, ws.OpText)
	// Chrome doesn't support fragmentation of incoming websocket messages. To
	// compensate this, they support single-fragment messages of up to 100MiB.
	//
	// See https://github.com/ChromeDevTools/devtools-protocol/issues/175.
	//
	// And according to https://bugs.chromium.org/p/chromium/issues/detail?id=1069431,
	// it seems like that fragmentation won't be supported very soon.
	// Luckily, now github.com/gobwas/ws will grow the buffer if needed.
	// The func name DisableFlush is a little misleading,
	// but it do make it grow the buffer if needed.
	c.writer.DisableFlush()

	// Perform marshal, reusing encoder
	var b bytes.Buffer
	c.encoder.Reset(&b, DefaultMarshalOptions)
	if err := jsonv2.MarshalEncode(&c.encoder, msg, DefaultMarshalOptions); err != nil {
		return err
	}

	// Write the bytes to the websocket.
	if c.debugf != nil {
		c.debugf("-> %s", b.Bytes())
	}
	if _, err := b.WriteTo(&c.writer); err != nil {
		return err
	}
	return c.writer.Flush()
}

// DialOption is a dial option.
type DialOption = func(*Conn)

// WithConnDebugf is a dial option to set a protocol logger.
func WithConnDebugf(f func(string, ...any)) DialOption {
	return func(c *Conn) {
		c.debugf = f
	}
}


================================================
FILE: contrib/docker-test.sh
================================================
#!/bin/bash

SRC=$(realpath $(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)/..)

pushd $SRC &> /dev/null

IMAGE=${IMAGE:-docker.io/chromedp/headless-shell:latest}

set -e

(set -x;
  CGO_ENABLED=0 go test -c
)

CMD=docker
if [ ! -z "$(type -p podman)" ]; then
  CMD=podman
fi

(set -x;
  $CMD run \
    --rm \
    --volume=$PWD:/chromedp \
    --entrypoint=/chromedp/chromedp.test \
    --workdir=/chromedp \
    --env=PATH=/headless-shell \
    --env=HEADLESS_SHELL=1 \
    $IMAGE -test.v -test.parallel=1 -test.timeout=3m
)

popd &> /dev/null


================================================
FILE: device/device.go
================================================
// Package device contains device emulation definitions for use with chromedp's
// Emulate action.
//
// See: https://raw.githubusercontent.com/puppeteer/puppeteer/main/packages/puppeteer-core/src/common/Device.ts
package device

// Code generated by gen.go. DO NOT EDIT.

//go:generate go run gen.go

// Info holds device information for use with chromedp.Emulate.
type Info struct {
	// Name is the device name.
	Name string

	// UserAgent is the device user agent string.
	UserAgent string

	// Width is the viewport width.
	Width int64

	// Height is the viewport height.
	Height int64

	// Scale is the device viewport scale factor.
	Scale float64

	// Landscape indicates whether or not the device is in landscape mode or
	// not.
	Landscape bool

	// Mobile indicates whether it is a mobile device or not.
	Mobile bool

	// Touch indicates whether the device has touch enabled.
	Touch bool
}

// String satisfies fmt.Stringer.
func (i Info) String() string {
	return i.Name
}

// Device satisfies chromedp.Device.
func (i Info) Device() Info {
	return i
}

// infoType provides the enumerated device type.
type infoType int

// String satisfies fmt.Stringer.
func (i infoType) String() string {
	return devices[i].String()
}

// Device satisfies chromedp.Device.
func (i infoType) Device() Info {
	return devices[i]
}

// Devices.
const (
	// Reset is the reset device.
	Reset infoType = iota

	// BlackberryPlayBook is the "Blackberry PlayBook" device.
	BlackberryPlayBook

	// BlackberryPlayBooklandscape is the "Blackberry PlayBook landscape" device.
	BlackberryPlayBooklandscape

	// BlackBerryZ30 is the "BlackBerry Z30" device.
	BlackBerryZ30

	// BlackBerryZ30landscape is the "BlackBerry Z30 landscape" device.
	BlackBerryZ30landscape

	// GalaxyNote3 is the "Galaxy Note 3" device.
	GalaxyNote3

	// GalaxyNote3landscape is the "Galaxy Note 3 landscape" device.
	GalaxyNote3landscape

	// GalaxyNoteII is the "Galaxy Note II" device.
	GalaxyNoteII

	// GalaxyNoteIIlandscape is the "Galaxy Note II landscape" device.
	GalaxyNoteIIlandscape

	// GalaxySIII is the "Galaxy S III" device.
	GalaxySIII

	// GalaxySIIIlandscape is the "Galaxy S III landscape" device.
	GalaxySIIIlandscape

	// GalaxyS5 is the "Galaxy S5" device.
	GalaxyS5

	// GalaxyS5landscape is the "Galaxy S5 landscape" device.
	GalaxyS5landscape

	// GalaxyS8 is the "Galaxy S8" device.
	GalaxyS8

	// GalaxyS8landscape is the "Galaxy S8 landscape" device.
	GalaxyS8landscape

	// GalaxyS9 is the "Galaxy S9+" device.
	GalaxyS9

	// GalaxyS9landscape is the "Galaxy S9+ landscape" device.
	GalaxyS9landscape

	// GalaxyTabS4 is the "Galaxy Tab S4" device.
	GalaxyTabS4

	// GalaxyTabS4landscape is the "Galaxy Tab S4 landscape" device.
	GalaxyTabS4landscape

	// IPad is the "iPad" device.
	IPad

	// IPadlandscape is the "iPad landscape" device.
	IPadlandscape

	// IPadgen6 is the "iPad (gen 6)" device.
	IPadgen6

	// IPadgen6landscape is the "iPad (gen 6) landscape" device.
	IPadgen6landscape

	// IPadgen7 is the "iPad (gen 7)" device.
	IPadgen7

	// IPadgen7landscape is the "iPad (gen 7) landscape" device.
	IPadgen7landscape

	// IPadMini is the "iPad Mini" device.
	IPadMini

	// IPadMinilandscape is the "iPad Mini landscape" device.
	IPadMinilandscape

	// IPadPro is the "iPad Pro" device.
	IPadPro

	// IPadProlandscape is the "iPad Pro landscape" device.
	IPadProlandscape

	// IPadPro11 is the "iPad Pro 11" device.
	IPadPro11

	// IPadPro11landscape is the "iPad Pro 11 landscape" device.
	IPadPro11landscape

	// IPhone4 is the "iPhone 4" device.
	IPhone4

	// IPhone4landscape is the "iPhone 4 landscape" device.
	IPhone4landscape

	// IPhone5 is the "iPhone 5" device.
	IPhone5

	// IPhone5landscape is the "iPhone 5 landscape" device.
	IPhone5landscape

	// IPhone6 is the "iPhone 6" device.
	IPhone6

	// IPhone6landscape is the "iPhone 6 landscape" device.
	IPhone6landscape

	// IPhone6Plus is the "iPhone 6 Plus" device.
	IPhone6Plus

	// IPhone6Pluslandscape is the "iPhone 6 Plus landscape" device.
	IPhone6Pluslandscape

	// IPhone7 is the "iPhone 7" device.
	IPhone7

	// IPhone7landscape is the "iPhone 7 landscape" device.
	IPhone7landscape

	// IPhone7Plus is the "iPhone 7 Plus" device.
	IPhone7Plus

	// IPhone7Pluslandscape is the "iPhone 7 Plus landscape" device.
	IPhone7Pluslandscape

	// IPhone8 is the "iPhone 8" device.
	IPhone8

	// IPhone8landscape is the "iPhone 8 landscape" device.
	IPhone8landscape

	// IPhone8Plus is the "iPhone 8 Plus" device.
	IPhone8Plus

	// IPhone8Pluslandscape is the "iPhone 8 Plus landscape" device.
	IPhone8Pluslandscape

	// IPhoneSE is the "iPhone SE" device.
	IPhoneSE

	// IPhoneSElandscape is the "iPhone SE landscape" device.
	IPhoneSElandscape

	// IPhoneX is the "iPhone X" device.
	IPhoneX

	// IPhoneXlandscape is the "iPhone X landscape" device.
	IPhoneXlandscape

	// IPhoneXR is the "iPhone XR" device.
	IPhoneXR

	// IPhoneXRlandscape is the "iPhone XR landscape" device.
	IPhoneXRlandscape

	// IPhone11 is the "iPhone 11" device.
	IPhone11

	// IPhone11landscape is the "iPhone 11 landscape" device.
	IPhone11landscape

	// IPhone11Pro is the "iPhone 11 Pro" device.
	IPhone11Pro

	// IPhone11Prolandscape is the "iPhone 11 Pro landscape" device.
	IPhone11Prolandscape

	// IPhone11ProMax is the "iPhone 11 Pro Max" device.
	IPhone11ProMax

	// IPhone11ProMaxlandscape is the "iPhone 11 Pro Max landscape" device.
	IPhone11ProMaxlandscape

	// IPhone12 is the "iPhone 12" device.
	IPhone12

	// IPhone12landscape is the "iPhone 12 landscape" device.
	IPhone12landscape

	// IPhone12Pro is the "iPhone 12 Pro" device.
	IPhone12Pro

	// IPhone12Prolandscape is the "iPhone 12 Pro landscape" device.
	IPhone12Prolandscape

	// IPhone12ProMax is the "iPhone 12 Pro Max" device.
	IPhone12ProMax

	// IPhone12ProMaxlandscape is the "iPhone 12 Pro Max landscape" device.
	IPhone12ProMaxlandscape

	// IPhone12Mini is the "iPhone 12 Mini" device.
	IPhone12Mini

	// IPhone12Minilandscape is the "iPhone 12 Mini landscape" device.
	IPhone12Minilandscape

	// IPhone13 is the "iPhone 13" device.
	IPhone13

	// IPhone13landscape is the "iPhone 13 landscape" device.
	IPhone13landscape

	// IPhone13Pro is the "iPhone 13 Pro" device.
	IPhone13Pro

	// IPhone13Prolandscape is the "iPhone 13 Pro landscape" device.
	IPhone13Prolandscape

	// IPhone13ProMax is the "iPhone 13 Pro Max" device.
	IPhone13ProMax

	// IPhone13ProMaxlandscape is the "iPhone 13 Pro Max landscape" device.
	IPhone13ProMaxlandscape

	// IPhone13Mini is the "iPhone 13 Mini" device.
	IPhone13Mini

	// IPhone13Minilandscape is the "iPhone 13 Mini landscape" device.
	IPhone13Minilandscape

	// IPhone14 is the "iPhone 14" device.
	IPhone14

	// IPhone14landscape is the "iPhone 14 landscape" device.
	IPhone14landscape

	// IPhone14Plus is the "iPhone 14 Plus" device.
	IPhone14Plus

	// IPhone14Pluslandscape is the "iPhone 14 Plus landscape" device.
	IPhone14Pluslandscape

	// IPhone14Pro is the "iPhone 14 Pro" device.
	IPhone14Pro

	// IPhone14Prolandscape is the "iPhone 14 Pro landscape" device.
	IPhone14Prolandscape

	// IPhone14ProMax is the "iPhone 14 Pro Max" device.
	IPhone14ProMax

	// IPhone14ProMaxlandscape is the "iPhone 14 Pro Max landscape" device.
	IPhone14ProMaxlandscape

	// IPhone15 is the "iPhone 15" device.
	IPhone15

	// IPhone15landscape is the "iPhone 15 landscape" device.
	IPhone15landscape

	// IPhone15Plus is the "iPhone 15 Plus" device.
	IPhone15Plus

	// IPhone15Pluslandscape is the "iPhone 15 Plus landscape" device.
	IPhone15Pluslandscape

	// IPhone15Pro is the "iPhone 15 Pro" device.
	IPhone15Pro

	// IPhone15Prolandscape is the "iPhone 15 Pro landscape" device.
	IPhone15Prolandscape

	// IPhone15ProMax is the "iPhone 15 Pro Max" device.
	IPhone15ProMax

	// IPhone15ProMaxlandscape is the "iPhone 15 Pro Max landscape" device.
	IPhone15ProMaxlandscape

	// JioPhone2 is the "JioPhone 2" device.
	JioPhone2

	// JioPhone2landscape is the "JioPhone 2 landscape" device.
	JioPhone2landscape

	// KindleFireHDX is the "Kindle Fire HDX" device.
	KindleFireHDX

	// KindleFireHDXlandscape is the "Kindle Fire HDX landscape" device.
	KindleFireHDXlandscape

	// LGOptimusL70 is the "LG Optimus L70" device.
	LGOptimusL70

	// LGOptimusL70landscape is the "LG Optimus L70 landscape" device.
	LGOptimusL70landscape

	// MicrosoftLumia550 is the "Microsoft Lumia 550" device.
	MicrosoftLumia550

	// MicrosoftLumia950 is the "Microsoft Lumia 950" device.
	MicrosoftLumia950

	// MicrosoftLumia950landscape is the "Microsoft Lumia 950 landscape" device.
	MicrosoftLumia950landscape

	// Nexus10 is the "Nexus 10" device.
	Nexus10

	// Nexus10landscape is the "Nexus 10 landscape" device.
	Nexus10landscape

	// Nexus4 is the "Nexus 4" device.
	Nexus4

	// Nexus4landscape is the "Nexus 4 landscape" device.
	Nexus4landscape

	// Nexus5 is the "Nexus 5" device.
	Nexus5

	// Nexus5landscape is the "Nexus 5 landscape" device.
	Nexus5landscape

	// Nexus5X is the "Nexus 5X" device.
	Nexus5X

	// Nexus5Xlandscape is the "Nexus 5X landscape" device.
	Nexus5Xlandscape

	// Nexus6 is the "Nexus 6" device.
	Nexus6

	// Nexus6landscape is the "Nexus 6 landscape" device.
	Nexus6landscape

	// Nexus6P is the "Nexus 6P" device.
	Nexus6P

	// Nexus6Plandscape is the "Nexus 6P landscape" device.
	Nexus6Plandscape

	// Nexus7 is the "Nexus 7" device.
	Nexus7

	// Nexus7landscape is the "Nexus 7 landscape" device.
	Nexus7landscape

	// NokiaLumia520 is the "Nokia Lumia 520" device.
	NokiaLumia520

	// NokiaLumia520landscape is the "Nokia Lumia 520 landscape" device.
	NokiaLumia520landscape

	// NokiaN9 is the "Nokia N9" device.
	NokiaN9

	// NokiaN9landscape is the "Nokia N9 landscape" device.
	NokiaN9landscape

	// Pixel2 is the "Pixel 2" device.
	Pixel2

	// Pixel2landscape is the "Pixel 2 landscape" device.
	Pixel2landscape

	// Pixel2XL is the "Pixel 2 XL" device.
	Pixel2XL

	// Pixel2XLlandscape is the "Pixel 2 XL landscape" device.
	Pixel2XLlandscape

	// Pixel3 is the "Pixel 3" device.
	Pixel3

	// Pixel3landscape is the "Pixel 3 landscape" device.
	Pixel3landscape

	// Pixel4 is the "Pixel 4" device.
	Pixel4

	// Pixel4landscape is the "Pixel 4 landscape" device.
	Pixel4landscape

	// Pixel4a5G is the "Pixel 4a (5G)" device.
	Pixel4a5G

	// Pixel4a5Glandscape is the "Pixel 4a (5G) landscape" device.
	Pixel4a5Glandscape

	// Pixel5 is the "Pixel 5" device.
	Pixel5

	// Pixel5landscape is the "Pixel 5 landscape" device.
	Pixel5landscape

	// MotoG4 is the "Moto G4" device.
	MotoG4

	// MotoG4landscape is the "Moto G4 landscape" device.
	MotoG4landscape
)

// devices is the list of devices.
var devices = [...]Info{
	{"", "", 0, 0, 0.000000, false, false, false},
	{"Blackberry PlayBook", "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+", 600, 1024, 1.000000, false, true, true},
	{"Blackberry PlayBook landscape", "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+", 1024, 600, 1.000000, true, true, true},
	{"BlackBerry Z30", "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+", 360, 640, 2.000000, false, true, true},
	{"BlackBerry Z30 landscape", "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+", 640, 360, 2.000000, true, true, true},
	{"Galaxy Note 3", "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", 360, 640, 3.000000, false, true, true},
	{"Galaxy Note 3 landscape", "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", 640, 360, 3.000000, true, true, true},
	{"Galaxy Note II", "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", 360, 640, 2.000000, false, true, true},
	{"Galaxy Note II landscape", "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", 640, 360, 2.000000, true, true, true},
	{"Galaxy S III", "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", 360, 640, 2.000000, false, true, true},
	{"Galaxy S III landscape", "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", 640, 360, 2.000000, true, true, true},
	{"Galaxy S5", "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 360, 640, 3.000000, false, true, true},
	{"Galaxy S5 landscape", "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 640, 360, 3.000000, true, true, true},
	{"Galaxy S8", "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", 360, 740, 3.000000, false, true, true},
	{"Galaxy S8 landscape", "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", 740, 360, 3.000000, true, true, true},
	{"Galaxy S9+", "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", 320, 658, 4.500000, false, true, true},
	{"Galaxy S9+ landscape", "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", 658, 320, 4.500000, true, true, true},
	{"Galaxy Tab S4", "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36", 712, 1138, 2.250000, false, true, true},
	{"Galaxy Tab S4 landscape", "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36", 1138, 712, 2.250000, true, true, true},
	{"iPad", "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1", 768, 1024, 2.000000, false, true, true},
	{"iPad landscape", "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1", 1024, 768, 2.000000, true, true, true},
	{"iPad (gen 6)", "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 768, 1024, 2.000000, false, true, true},
	{"iPad (gen 6) landscape", "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 1024, 768, 2.000000, true, true, true},
	{"iPad (gen 7)", "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 810, 1080, 2.000000, false, true, true},
	{"iPad (gen 7) landscape", "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 1080, 810, 2.000000, true, true, true},
	{"iPad Mini", "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1", 768, 1024, 2.000000, false, true, true},
	{"iPad Mini landscape", "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1", 1024, 768, 2.000000, true, true, true},
	{"iPad Pro", "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1", 1024, 1366, 2.000000, false, true, true},
	{"iPad Pro landscape", "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1", 1366, 1024, 2.000000, true, true, true},
	{"iPad Pro 11", "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 834, 1194, 2.000000, false, true, true},
	{"iPad Pro 11 landscape", "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 1194, 834, 2.000000, true, true, true},
	{"iPhone 4", "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53", 320, 480, 2.000000, false, true, true},
	{"iPhone 4 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53", 480, 320, 2.000000, true, true, true},
	{"iPhone 5", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1", 320, 568, 2.000000, false, true, true},
	{"iPhone 5 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1", 568, 320, 2.000000, true, true, true},
	{"iPhone 6", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 375, 667, 2.000000, false, true, true},
	{"iPhone 6 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 667, 375, 2.000000, true, true, true},
	{"iPhone 6 Plus", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 414, 736, 3.000000, false, true, true},
	{"iPhone 6 Plus landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 736, 414, 3.000000, true, true, true},
	{"iPhone 7", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 375, 667, 2.000000, false, true, true},
	{"iPhone 7 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 667, 375, 2.000000, true, true, true},
	{"iPhone 7 Plus", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 414, 736, 3.000000, false, true, true},
	{"iPhone 7 Plus landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 736, 414, 3.000000, true, true, true},
	{"iPhone 8", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 375, 667, 2.000000, false, true, true},
	{"iPhone 8 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 667, 375, 2.000000, true, true, true},
	{"iPhone 8 Plus", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 414, 736, 3.000000, false, true, true},
	{"iPhone 8 Plus landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 736, 414, 3.000000, true, true, true},
	{"iPhone SE", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1", 320, 568, 2.000000, false, true, true},
	{"iPhone SE landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1", 568, 320, 2.000000, true, true, true},
	{"iPhone X", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 375, 812, 3.000000, false, true, true},
	{"iPhone X landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", 812, 375, 3.000000, true, true, true},
	{"iPhone XR", "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1", 414, 896, 3.000000, false, true, true},
	{"iPhone XR landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1", 896, 414, 3.000000, true, true, true},
	{"iPhone 11", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1", 414, 828, 2.000000, false, true, true},
	{"iPhone 11 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1", 828, 414, 2.000000, true, true, true},
	{"iPhone 11 Pro", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1", 375, 812, 3.000000, false, true, true},
	{"iPhone 11 Pro landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1", 812, 375, 3.000000, true, true, true},
	{"iPhone 11 Pro Max", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1", 414, 896, 3.000000, false, true, true},
	{"iPhone 11 Pro Max landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1", 896, 414, 3.000000, true, true, true},
	{"iPhone 12", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 390, 844, 3.000000, false, true, true},
	{"iPhone 12 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 844, 390, 3.000000, true, true, true},
	{"iPhone 12 Pro", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 390, 844, 3.000000, false, true, true},
	{"iPhone 12 Pro landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 844, 390, 3.000000, true, true, true},
	{"iPhone 12 Pro Max", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 428, 926, 3.000000, false, true, true},
	{"iPhone 12 Pro Max landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 926, 428, 3.000000, true, true, true},
	{"iPhone 12 Mini", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 375, 812, 3.000000, false, true, true},
	{"iPhone 12 Mini landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 812, 375, 3.000000, true, true, true},
	{"iPhone 13", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 390, 844, 3.000000, false, true, true},
	{"iPhone 13 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 844, 390, 3.000000, true, true, true},
	{"iPhone 13 Pro", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 390, 844, 3.000000, false, true, true},
	{"iPhone 13 Pro landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 844, 390, 3.000000, true, true, true},
	{"iPhone 13 Pro Max", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 428, 926, 3.000000, false, true, true},
	{"iPhone 13 Pro Max landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 926, 428, 3.000000, true, true, true},
	{"iPhone 13 Mini", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 375, 812, 3.000000, false, true, true},
	{"iPhone 13 Mini landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1", 812, 375, 3.000000, true, true, true},
	{"iPhone 14", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", 390, 663, 3.000000, false, true, true},
	{"iPhone 14 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", 750, 340, 3.000000, true, true, true},
	{"iPhone 14 Plus", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", 428, 745, 3.000000, false, true, true},
	{"iPhone 14 Plus landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", 832, 378, 3.000000, true, true, true},
	{"iPhone 14 Pro", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", 393, 659, 3.000000, false, true, true},
	{"iPhone 14 Pro landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", 734, 343, 3.000000, true, true, true},
	{"iPhone 14 Pro Max", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", 430, 739, 3.000000, false, true, true},
	{"iPhone 14 Pro Max landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", 814, 380, 3.000000, true, true, true},
	{"iPhone 15", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", 393, 659, 3.000000, false, true, true},
	{"iPhone 15 landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", 734, 343, 3.000000, true, true, true},
	{"iPhone 15 Plus", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", 430, 739, 3.000000, false, true, true},
	{"iPhone 15 Plus landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", 814, 380, 3.000000, true, true, true},
	{"iPhone 15 Pro", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", 393, 659, 3.000000, false, true, true},
	{"iPhone 15 Pro landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", 734, 343, 3.000000, true, true, true},
	{"iPhone 15 Pro Max", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", 430, 739, 3.000000, false, true, true},
	{"iPhone 15 Pro Max landscape", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", 814, 380, 3.000000, true, true, true},
	{"JioPhone 2", "Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5", 240, 320, 1.000000, false, true, true},
	{"JioPhone 2 landscape", "Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5", 320, 240, 1.000000, true, true, true},
	{"Kindle Fire HDX", "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true", 800, 1280, 2.000000, false, true, true},
	{"Kindle Fire HDX landscape", "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true", 1280, 800, 2.000000, true, true, true},
	{"LG Optimus L70", "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36", 384, 640, 1.250000, false, true, true},
	{"LG Optimus L70 landscape", "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36", 640, 384, 1.250000, true, true, true},
	{"Microsoft Lumia 550", "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263", 640, 360, 2.000000, false, true, true},
	{"Microsoft Lumia 950", "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263", 360, 640, 4.000000, false, true, true},
	{"Microsoft Lumia 950 landscape", "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263", 640, 360, 4.000000, true, true, true},
	{"Nexus 10", "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36", 800, 1280, 2.000000, false, true, true},
	{"Nexus 10 landscape", "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36", 1280, 800, 2.000000, true, true, true},
	{"Nexus 4", "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 384, 640, 2.000000, false, true, true},
	{"Nexus 4 landscape", "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 640, 384, 2.000000, true, true, true},
	{"Nexus 5", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 360, 640, 3.000000, false, true, true},
	{"Nexus 5 landscape", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 640, 360, 3.000000, true, true, true},
	{"Nexus 5X", "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 412, 732, 2.625000, false, true, true},
	{"Nexus 5X landscape", "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 732, 412, 2.625000, true, true, true},
	{"Nexus 6", "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 412, 732, 3.500000, false, true, true},
	{"Nexus 6 landscape", "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 732, 412, 3.500000, true, true, true},
	{"Nexus 6P", "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 412, 732, 3.500000, false, true, true},
	{"Nexus 6P landscape", "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 732, 412, 3.500000, true, true, true},
	{"Nexus 7", "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36", 600, 960, 2.000000, false, true, true},
	{"Nexus 7 landscape", "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36", 960, 600, 2.000000, true, true, true},
	{"Nokia Lumia 520", "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)", 320, 533, 1.500000, false, true, true},
	{"Nokia Lumia 520 landscape", "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)", 533, 320, 1.500000, true, true, true},
	{"Nokia N9", "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13", 480, 854, 1.000000, false, true, true},
	{"Nokia N9 landscape", "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13", 854, 480, 1.000000, true, true, true},
	{"Pixel 2", "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 411, 731, 2.625000, false, true, true},
	{"Pixel 2 landscape", "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 731, 411, 2.625000, true, true, true},
	{"Pixel 2 XL", "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 411, 823, 3.500000, false, true, true},
	{"Pixel 2 XL landscape", "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36", 823, 411, 3.500000, true, true, true},
	{"Pixel 3", "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36", 393, 786, 2.750000, false, true, true},
	{"Pixel 3 landscape", "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36", 786, 393, 2.750000, true, true, true},
	{"Pixel 4", "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36", 353, 745, 3.000000, false, true, true},
	{"Pixel 4 landscape", "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36", 745, 353, 3.000000, true, true, true},
	{"Pixel 4a (5G)", "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36", 353, 745, 3.000000, false, true, true},
	{"Pixel 4a (5G) landscape", "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36", 745, 353, 3.000000, true, true, true},
	{"Pixel 5", "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36", 393, 851, 3.000000, false, true, true},
	{"Pixel 5 landscape", "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36", 851, 393, 3.000000, true, true, true},
	{"Moto G4", "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36", 360, 640, 3.000000, false, true, true},
	{"Moto G4 landscape", "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36", 640, 360, 3.000000, true, true, true},
}


================================================
FILE: device/gen.go
================================================
//go:build ignore
// +build ignore

package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"go/format"
	"io"
	"net/http"
	"os"
	"regexp"
	"strings"
)

const deviceDescriptorsURL = "https://raw.githubusercontent.com/puppeteer/puppeteer/main/packages/puppeteer-core/src/common/Device.ts"

func main() {
	out := flag.String("out", "device.go", "out")
	flag.Parse()
	if err := run(*out); err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
}

type deviceDescriptor struct {
	Name      string `json:"name"`
	UserAgent string `json:"userAgent"`
	Viewport  struct {
		Width             int64   `json:"width"`
		Height            int64   `json:"height"`
		DeviceScaleFactor float64 `json:"deviceScaleFactor"`
		IsMobile          bool    `json:"isMobile"`
		HasTouch          bool    `json:"hasTouch"`
		IsLandscape       bool    `json:"isLandscape"`
	} `json:"viewport"`
}

var cleanRE = regexp.MustCompile(`[^a-zA-Z0-9_]`)

// run runs the program.
func run(out string) error {
	descriptors, err := get()
	if err != nil {
		return err
	}
	// add reset device
	descriptors = append([]deviceDescriptor{{}}, descriptors...)
	buf := new(bytes.Buffer)
	fmt.Fprintf(buf, hdr, deviceDescriptorsURL)
	fmt.Fprintln(buf, "\n// Devices.")
	fmt.Fprintln(buf, "const (")
	for i, d := range descriptors {
		if i == 0 {
			fmt.Fprintln(buf, "// Reset is the reset device.")
			fmt.Fprintln(buf, "Reset infoType = iota\n")
		} else {
			name := cleanRE.ReplaceAllString(d.Name, "")
			name = strings.ToUpper(name[0:1]) + name[1:]
			fmt.Fprintf(buf, "// %s is the %q device.\n", name, d.Name)
			fmt.Fprintf(buf, "%s\n\n", name)
		}
	}
	fmt.Fprintln(buf, ")\n")
	fmt.Fprintln(buf, "// devices is the list of devices.")
	fmt.Fprintln(buf, "var devices = [...]Info{")
	for _, d := range descriptors {
		fmt.Fprintf(buf, "{%q, %q, %d, %d, %f, %t, %t, %t},\n",
			d.Name, d.UserAgent,
			d.Viewport.Width, d.Viewport.Height, d.Viewport.DeviceScaleFactor,
			d.Viewport.IsLandscape, d.Viewport.IsMobile, d.Viewport.HasTouch,
		)
	}
	fmt.Fprintln(buf, "}")
	src, err := format.Source(buf.Bytes())
	if err != nil {
		return err
	}
	return os.WriteFile(out, src, 0o644)
}

var (
	startRE        = regexp.MustCompile(`(?m)^const\s+knownDevices\s*=\s*\[`)
	endRE          = regexp.MustCompile(`(?m)^\] as const;`)
	fixLandscapeRE = regexp.MustCompile(`isLandscape:\s*(true|false),`)
	fixKeysRE      = regexp.MustCompile(`(?m)^(\s+)([a-zA-Z]+):`)
	fixClosesRE    = regexp.MustCompile(`([\]\}]),\n(\s*[\]\}])`)
)

// get retrieves and decodes the device descriptors.
func get() ([]deviceDescriptor, error) {
	res, err := http.Get(deviceDescriptorsURL)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != 200 {
		return nil, fmt.Errorf("got status code %d", res.StatusCode)
	}
	buf, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	start := startRE.FindIndex(buf)
	if start == nil {
		return nil, errors.New("could not find start")
	}
	buf = buf[start[1]-1:]
	end := endRE.FindIndex(buf)
	if end == nil {
		return nil, errors.New("could not find end")
	}
	buf = buf[:end[1]-10]
	buf = bytes.Replace(buf, []byte("'"), []byte(`"`), -1)
	buf = fixLandscapeRE.ReplaceAll(buf, []byte(`"isLandscape": $1`))
	buf = fixKeysRE.ReplaceAll(buf, []byte(`$1"$2":`))
	buf = fixClosesRE.ReplaceAll(buf, []byte("$1\n$2"))
	buf = fixClosesRE.ReplaceAll(buf, []byte("$1\n$2"))
	var descriptors []deviceDescriptor
	if err := json.Unmarshal(buf, &descriptors); err != nil {
		return nil, err
	}
	return descriptors, nil
}

const hdr = `// Package device contains device emulation definitions for use with chromedp's
// Emulate action.
//
// See: %s
package device

` + `// Code generated by gen.go. DO NOT EDIT.` + `

//go:generate go run gen.go

// Info holds device information for use with chromedp.Emulate.
type Info struct {
	// Name is the device name.
	Name string

	// UserAgent is the device user agent string.
	UserAgent string

	// Width is the viewport width.
	Width int64

	// Height is the viewport height.
	Height int64

	// Scale is the device viewport scale factor.
	Scale float64

	// Landscape indicates whether or not the device is in landscape mode or
	// not.
	Landscape bool

	// Mobile indicates whether it is a mobile device or not.
	Mobile bool

	// Touch indicates whether the device has touch enabled.
	Touch bool
}

// String satisfies fmt.Stringer.
func (i Info) String() string {
	return i.Name
}

// Device satisfies chromedp.Device.
func (i Info) Device() Info {
	return i
}

// infoType provides the enumerated device type.
type infoType int

// String satisfies fmt.Stringer.
func (i infoType) String() string {
	return devices[i].String()
}

// Device satisfies chromedp.Device.
func (i infoType) Device() Info {
	return devices[i]
}

`


================================================
FILE: emulate.go
================================================
package chromedp

import (
	"github.com/chromedp/cdproto/emulation"
	"github.com/chromedp/chromedp/device"
)

// EmulateAction are actions that change the emulation settings for the
// browser.
type EmulateAction Action

// EmulateViewport is an action to change the browser viewport.
//
// Wraps calls to emulation.SetDeviceMetricsOverride and emulation.SetTouchEmulationEnabled.
//
// Note: this has the effect of setting/forcing the screen orientation to
// landscape, and will disable mobile and touch emulation by default. If this
// is not the desired behavior, use the emulate viewport options
// EmulateOrientation (or EmulateLandscape/EmulatePortrait), EmulateMobile, and
// EmulateTouch, respectively.
func EmulateViewport(width, height int64, opts ...EmulateViewportOption) EmulateAction {
	p1 := emulation.SetDeviceMetricsOverride(width, height, 1.0, false)
	p2 := emulation.SetTouchEmulationEnabled(false)
	for _, o := range opts {
		o(p1, p2)
	}
	return Tasks{p1, p2}
}

// EmulateViewportOption is the type for emulate viewport options.
type EmulateViewportOption = func(*emulation.SetDeviceMetricsOverrideParams, *emulation.SetTouchEmulationEnabledParams)

// EmulateScale is an emulate viewport option to set the device viewport scaling
// factor.
func EmulateScale(scale float64) EmulateViewportOption {
	return func(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *emulation.SetTouchEmulationEnabledParams) {
		p1.DeviceScaleFactor = scale
	}
}

// EmulateOrientation is an emulate viewport option to set the device viewport
// screen orientation.
func EmulateOrientation(orientation emulation.OrientationType, angle int64) EmulateViewportOption {
	return func(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *emulation.SetTouchEmulationEnabledParams) {
		p1.ScreenOrientation = &emulation.ScreenOrientation{
			Type:  orientation,
			Angle: angle,
		}
	}
}

// EmulateLandscape is an emulate viewport option to set the device viewport
// screen orientation in landscape primary mode and an angle of 90.
func EmulateLandscape(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *emulation.SetTouchEmulationEnabledParams) {
	EmulateOrientation(emulation.OrientationTypeLandscapePrimary, 90)(p1, p2)
}

// EmulatePortrait is an emulate viewport option to set the device viewport
// screen orientation in portrait primary mode and an angle of 0.
func EmulatePortrait(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *emulation.SetTouchEmulationEnabledParams) {
	EmulateOrientation(emulation.OrientationTypePortraitPrimary, 0)(p1, p2)
}

// EmulateMobile is an emulate viewport option to toggle the device viewport to
// display as a mobile device.
func EmulateMobile(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *emulation.SetTouchEmulationEnabledParams) {
	p1.Mobile = true
}

// EmulateTouch is an emulate viewport option to enable touch emulation.
func EmulateTouch(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *emulation.SetTouchEmulationEnabledParams) {
	p2.Enabled = true
}

// ResetViewport is an action to reset the browser viewport to the default
// values the browser was started with.
//
// Note: does not modify / change the browser's emulated User-Agent, if any.
func ResetViewport() EmulateAction {
	return EmulateViewport(0, 0, EmulatePortrait)
}

// Device is the shared interface for known device types.
//
// See [device] for a set of off-the-shelf devices and modes.
type Device interface {
	// Device returns the device info.
	Device() device.Info
}

// Emulate is an action to emulate a specific device.
//
// See [device] for a set of off-the-shelf devices and modes.
func Emulate(device Device) EmulateAction {
	d := device.Device()

	var angle int64
	orientation := emulation.OrientationTypePortraitPrimary
	if d.Landscape {
		orientation, angle = emulation.OrientationTypeLandscapePrimary, 90
	}

	return Tasks{
		emulation.SetUserAgentOverride(d.UserAgent),
		emulation.SetDeviceMetricsOverride(d.Width, d.Height, d.Scale, d.Mobile).
			WithScreenOrientation(&emulation.ScreenOrientation{
				Type:  orientation,
				Angle: angle,
			}),
		emulation.SetTouchEmulationEnabled(d.Touch),
	}
}

// EmulateReset is an action to reset the device emulation.
//
// Resets the browser's viewport, screen orientation, user-agent, and
// mobile/touch emulation settings to the original values the browser was
// started with.
func EmulateReset() EmulateAction {
	return Emulate(device.Reset)
}


================================================
FILE: emulate_test.go
================================================
package chromedp

import (
	"bytes"
	"image/png"
	"testing"

	"github.com/chromedp/chromedp/device"
)

func TestEmulate(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "image.html")
	defer cancel()

	var buf []byte
	if err := Run(ctx,
		Emulate(device.IPhone7),
		Screenshot(`#half-color`, &buf, ByID),
	); err != nil {
		t.Fatal(err)
	}

	img, err := png.Decode(bytes.NewReader(buf))
	if err != nil {
		t.Fatal(err)
	}
	size := img.Bounds().Size()
	if size.X != 400 || size.Y != 400 {
		t.Errorf("expected size 400x400, got: %dx%d", size.X, size.Y)
	}
}


================================================
FILE: errors.go
================================================
package chromedp

// Error is a chromedp error.
type Error string

// Error satisfies the error interface.
func (err Error) Error() string {
	return string(err)
}

// Error types.
const (
	// ErrInvalidWebsocketMessage is the invalid websocket message.
	ErrInvalidWebsocketMessage Error = "invalid websocket message"

	// ErrInvalidDimensions is the invalid dimensions error.
	ErrInvalidDimensions Error = "invalid dimensions"

	// ErrNoResults is the no results error.
	ErrNoResults Error = "no results"

	// ErrHasResults is the has results error.
	ErrHasResults Error = "has results"

	// ErrNotVisible is the not visible error.
	ErrNotVisible Error = "not visible"

	// ErrVisible is the visible error.
	ErrVisible Error = "visible"

	// ErrDisabled is the disabled error.
	ErrDisabled Error = "disabled"

	// ErrNotSelected is the not selected error.
	ErrNotSelected Error = "not selected"

	// ErrInvalidBoxModel is the invalid box model error.
	ErrInvalidBoxModel Error = "invalid box model"

	// ErrChannelClosed is the channel closed error.
	ErrChannelClosed Error = "channel closed"

	// ErrInvalidTarget is the invalid target error.
	ErrInvalidTarget Error = "invalid target"

	// ErrInvalidContext is the invalid context error.
	ErrInvalidContext Error = "invalid context"

	// ErrPollingTimeout is the error that the timeout reached before the pageFunction returns a truthy value.
	ErrPollingTimeout Error = "waiting for function failed: timeout"

	// ErrJSUndefined is the error that the type of RemoteObject is "undefined".
	ErrJSUndefined Error = "encountered an undefined value"

	// ErrJSNull is the error that the value of RemoteObject is null.
	ErrJSNull Error = "encountered a null value"
)


================================================
FILE: eval.go
================================================
package chromedp

import (
	"context"
	"encoding/json"
	"reflect"

	"github.com/chromedp/cdproto/runtime"
)

// EvaluateAction are actions that evaluate JavaScript expressions using
// runtime.Evaluate.
type EvaluateAction Action

// Evaluate is an action to evaluate the JavaScript expression, unmarshaling
// the result of the script evaluation to res.
//
// When res is nil, the script result will be ignored.
//
// When res is a *[]byte, the raw JSON-encoded value of the script
// result will be placed in res.
//
// When res is a **runtime.RemoteObject, res will be set to the low-level
// protocol type, and no attempt will be made to convert the result.
// The original objects could be maintained in memory until the page is
// navigated or closed. `runtime.ReleaseObject` or `runtime.ReleaseObjectGroup`
// can be used to ask the browser to release the original objects.
//
// For all other cases, the result of the script will be returned "by value" (i.e.,
// JSON-encoded), and subsequently an attempt will be made to json.Unmarshal
// the script result to res. When the script result is "undefined" or "null",
// and the value that res points to can not be nil (only the value of a chan,
// func, interface, map, pointer, or slice can be nil), it returns [ErrJSUndefined]
// or [ErrJSNull] respectively.
func Evaluate(expression string, res any, opts ...EvaluateOption) EvaluateAction {
	return ActionFunc(func(ctx context.Context) error {
		// set up parameters
		p := runtime.Evaluate(expression)
		switch res.(type) {
		case **runtime.RemoteObject:
		default:
			p = p.WithReturnByValue(true)
		}

		// apply opts
		for _, o := range opts {
			p = o(p)
		}

		// evaluate
		v, exp, err := p.Do(ctx)
		if err != nil {
			return err
		}
		if exp != nil {
			return exp
		}

		return parseRemoteObject(v, res)
	})
}

func parseRemoteObject(v *runtime.RemoteObject, res any) (err error) {
	if res == nil {
		return
	}

	switch x := res.(type) {
	case **runtime.RemoteObject:
		*x = v
		return

	case *[]byte:
		*x = v.Value
		return
	}

	value := v.Value
	if value == nil {
		rv := reflect.ValueOf(res)
		if rv.Kind() == reflect.Ptr {
			switch rv.Elem().Kind() {
			// Common kinds that can be nil.
			case reflect.Ptr, reflect.Map, reflect.Slice:
			// It's weird that res is a pointer to the following kinds,
			// but they can be nil too.
			case reflect.Chan, reflect.Func, reflect.Interface:
			default:
				// When the value that `res` points to can not be set to nil,
				// return [ErrJSUndefined] or [ErrJSNull] respectively.
				if v.Type == "undefined" {
					return ErrJSUndefined
				}
				return ErrJSNull
			}
		}
		// Change the value to the json literal null to make json.Unmarshal happy.
		value = []byte("null")
	}

	return json.Unmarshal(value, res)
}

// EvaluateAsDevTools is an action that evaluates a JavaScript expression as
// Chrome DevTools would, evaluating the expression in the "console" context,
// and making the Command Line API available to the script.
//
// See [Evaluate] for more information on how script expressions are evaluated.
//
// Note: this should not be used with untrusted JavaScript.
func EvaluateAsDevTools(expression string, res any, opts ...EvaluateOption) EvaluateAction {
	return Evaluate(expression, res, append(opts, EvalObjectGroup("console"), EvalWithCommandLineAPI)...)
}

// EvaluateOption is the type for JavaScript evaluation options.
type EvaluateOption = func(*runtime.EvaluateParams) *runtime.EvaluateParams

// EvalObjectGroup is an evaluate option to set the object group.
func EvalObjectGroup(objectGroup string) EvaluateOption {
	return func(p *runtime.EvaluateParams) *runtime.EvaluateParams {
		return p.WithObjectGroup(objectGroup)
	}
}

// EvalWithCommandLineAPI is an evaluate option to make the DevTools Command
// Line API available to the evaluated script.
//
// See [Evaluate] for more information on how evaluate actions work.
//
// Note: this should not be used with untrusted JavaScript.
func EvalWithCommandLineAPI(p *runtime.EvaluateParams) *runtime.EvaluateParams {
	return p.WithIncludeCommandLineAPI(true)
}

// EvalIgnoreExceptions is an evaluate option that will cause JavaScript
// evaluation to ignore exceptions.
func EvalIgnoreExceptions(p *runtime.EvaluateParams) *runtime.EvaluateParams {
	return p.WithSilent(true)
}

// EvalAsValue is an evaluate option that will cause the evaluated JavaScript
// expression to encode the result of the expression as a JSON-encoded value.
func EvalAsValue(p *runtime.EvaluateParams) *runtime.EvaluateParams {
	return p.WithReturnByValue(true)
}


================================================
FILE: eval_test.go
================================================
package chromedp

import (
	"reflect"
	"testing"

	"github.com/chromedp/cdproto/runtime"
)

func TestEvaluateNumber(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		expression string
		res        int
		want       int
		wantErr    string
	}{
		{
			name:       "normal",
			expression: "123",
			want:       123,
		},
		{
			name:       "undefined",
			expression: "",
			wantErr:    "encountered an undefined value",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx, cancel := testAllocate(t, "")
			defer cancel()

			err := Run(ctx,
				Evaluate(test.expression, &test.res),
			)
			if test.wantErr == "" && err != nil {
				t.Fatalf("got error: %v", err)
			}
			if test.wantErr != "" && (err == nil || test.wantErr != err.Error()) {
				t.Fatalf("wanted error: %q, got: %q", test.wantErr, err)
			} else if test.res != test.want {
				t.Fatalf("want: %v, got: %v", test.want, test.res)
			}
		})
	}
}

func TestEvaluateString(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		expression string
		res        string
		want       string
		wantErr    string
	}{
		{
			name:       "normal",
			expression: "'str'",
			want:       "str",
		},
		{
			name:       "undefined",
			expression: "",
			wantErr:    "encountered an undefined value",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx, cancel := testAllocate(t, "")
			defer cancel()

			err := Run(ctx,
				Evaluate(test.expression, &test.res),
			)
			if test.wantErr == "" && err != nil {
				t.Fatalf("got error: %v", err)
			}
			if test.wantErr != "" && (err == nil || test.wantErr != err.Error()) {
				t.Fatalf("wanted error: %q, got: %q", test.wantErr, err)
			} else if test.res != test.want {
				t.Fatalf("want: %v, got: %v", test.want, test.res)
			}
		})
	}
}

func TestEvaluateBytes(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		expression string
		res        []byte
		want       []byte
	}{
		{
			name:       "normal",
			expression: "'bytes'",
			want:       []byte(`"bytes"`),
		},
		{
			name:       "undefined",
			expression: "",
			want:       []byte(nil),
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx, cancel := testAllocate(t, "")
			defer cancel()

			err := Run(ctx,
				Evaluate(test.expression, &test.res),
			)
			if err != nil {
				t.Fatalf("got error: %v", err)
			}
			if !reflect.DeepEqual(test.res, test.want) {
				t.Fatalf("want: %v, got: %v", test.want, test.res)
			}
		})
	}
}

func TestEvaluateRemoteObject(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		expression string
		res        *runtime.RemoteObject
		wantType   string
	}{
		{
			name:       "object",
			expression: "window",
			wantType:   "object",
		},
		{
			name:       "function",
			expression: "window.alert",
			wantType:   "function",
		},
		{
			name:       "undefined",
			expression: "",
			wantType:   "undefined",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx, cancel := testAllocate(t, "")
			defer cancel()

			err := Run(ctx,
				Evaluate(test.expression, &test.res),
			)
			if err != nil {
				t.Fatalf("got error: %v", err)
			}
			if string(test.res.Type) != test.wantType {
				t.Fatalf("want type: %v, got type: %v", test.wantType, test.res.Type)
			}
		})
	}
}

func TestEvaluateNil(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		expression string
	}{
		{
			name:       "number",
			expression: "123",
		},
		{
			name:       "string",
			expression: "'str'",
		},
		{
			name:       "undefined",
			expression: "",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx, cancel := testAllocate(t, "")
			defer cancel()

			err := Run(ctx,
				Evaluate(test.expression, nil),
			)
			if err != nil {
				t.Fatalf("got error: %v", err)
			}
		})
	}
}


================================================
FILE: event_test.go
================================================
package chromedp

import (
	"strings"
	"testing"

	"github.com/chromedp/cdproto/page"
	"github.com/chromedp/cdproto/target"
)

func TestCloseDialog(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		accept     bool
		promptText string
		dialogType page.DialogType
		sel        string
		want       string
	}{
		{
			name:       "AlertAcceptWithPromptText",
			accept:     true,
			promptText: "this is a prompt text",
			dialogType: page.DialogTypeAlert,
			sel:        "#alert",
			want:       "alert text",
		},
		{
			name:       "AlertDismissWithPromptText",
			accept:     false,
			promptText: "this is a prompt text",
			dialogType: page.DialogTypeAlert,
			sel:        "#alert",
			want:       "alert text",
		},
		{
			name:       "AlertAcceptWithoutPromptText",
			accept:     true,
			dialogType: page.DialogTypeAlert,
			sel:        "#alert",
			want:       "alert text",
		},
		{
			name:       "AlertDismissWithoutPromptText",
			accept:     false,
			dialogType: page.DialogTypeAlert,
			sel:        "#alert",
			want:       "alert text",
		},
		{
			name:       "PromptAcceptWithPromptText",
			accept:     true,
			promptText: "this is a prompt text",
			dialogType: page.DialogTypePrompt,
			sel:        "#prompt",
			want:       "prompt text",
		},
		{
			name:       "PromptDismissWithPromptText",
			accept:     false,
			promptText: "this is a prompt text",
			dialogType: page.DialogTypePrompt,
			sel:        "#prompt",
			want:       "prompt text",
		},
		{
			name:       "PromptAcceptWithoutPromptText",
			accept:     true,
			dialogType: page.DialogTypePrompt,
			sel:        "#prompt",
			want:       "prompt text",
		},
		{
			name:       "PromptDismissWithoutPromptText",
			accept:     false,
			dialogType: page.DialogTypePrompt,
			sel:        "#prompt",
			want:       "prompt text",
		},
		{
			name:       "ConfirmAcceptWithPromptText",
			accept:     true,
			promptText: "this is a prompt text",
			dialogType: page.DialogTypeConfirm,
			sel:        "#confirm",
			want:       "confirm text",
		},
		{
			name:       "ConfirmDismissWithPromptText",
			accept:     false,
			promptText: "this is a prompt text",
			dialogType: page.DialogTypeConfirm,
			sel:        "#confirm",
			want:       "confirm text",
		},
		{
			name:       "ConfirmAcceptWithoutPromptText",
			accept:     true,
			dialogType: page.DialogTypeConfirm,
			sel:        "#confirm",
			want:       "confirm text",
		},
		{
			name:       "ConfirmDismissWithoutPromptText",
			accept:     false,
			dialogType: page.DialogTypeConfirm,
			sel:        "#confirm",
			want:       "confirm text",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			ctx, cancel := testAllocate(t, "")
			defer cancel()

			ListenTarget(ctx, func(ev any) {
				switch e := ev.(type) {
				case *page.EventJavascriptDialogOpening:
					if e.Type != test.dialogType {
						t.Errorf("expected dialog type to be %q, got: %q", test.dialogType, e.Type)
					}
					if e.Message != test.want {
						t.Errorf("expected dialog message to be %q, got: %q", test.want, e.Message)
					}

					task := page.HandleJavaScriptDialog(test.accept)
					if test.promptText != "" {
						task = task.WithPromptText(test.promptText)
					}
					go func() {
						if err := Run(ctx, task); err != nil {
							t.Error(err)
						}
					}()
				case *page.EventJavascriptDialogClosed:
					if e.Result != test.accept {
						t.Errorf("expected result to be %t, got %t", test.accept, e.Result)
					}
					if e.UserInput != test.promptText {
						t.Errorf("expected user input to be %q, got %q", test.promptText, e.UserInput)
					}
				}
			})

			if err := Run(ctx,
				Navigate(testdataDir+"/dialog.html"),
				Click(test.sel, ByID, NodeVisible),
			); err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestWaitNewTarget(t *testing.T) {
	t.Parallel()

	ctx, cancel := testAllocate(t, "newtab.html")
	defer cancel()

	ch := WaitNewTarget(ctx, func(info *target.Info) bool {
		return info.URL != ""
	})
	if err := Run(ctx, Click("#new-tab", ByID)); err != nil {
		t.Fatal(err)
	}
	blankCtx, cancel := NewContext(ctx, WithTargetID(<-ch))
	defer cancel()

	var urlstr string
	if err := Run(blankCtx,
		Location(&urlstr),
		WaitVisible(`#form`, ByID),
	); err != nil {
		t.Fatal(err)
	}
	if !strings.HasSuffix(urlstr, "form.html") {
		t.Errorf("want to be on form.html, at %q", urlstr)
	}
}


================================================
FILE: example_test.go
================================================
package chromedp_test

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/chromedp/cdproto/cdp"
	"github.com/chromedp/cdproto/dom"
	"github.com/chromedp/cdproto/page"
	"github.com/chromedp/cdproto/runtime"
	"github.com/chromedp/cdproto/target"
	"github.com/chromedp/chromedp"
	"github.com/chromedp/chromedp/device"
)

func writeHTML(content string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")
		io.WriteString(w, strings.TrimSpace(content))
	})
}

func ExampleTitle() {
	ctx, cancel := chromedp.NewContext(context.Background())
	defer cancel()

	ts := httptest.NewServer(writeHTML(`
<head>
	<title>fancy website title</title>
</head>
<body>
	<div id="content"></div>
</body>
	`))
	defer ts.Close()

	var title string
	if err := chromedp.Run(ctx,
		chromedp.Navigate(ts.URL),
		chromedp.Title(&title),
	); err != nil {
		log.Fatal(err)
	}
	fmt.Println(title)

	// Output:
	// fancy website title
}

func ExampleRunResponse() {
	ctx, cancel := chromedp.NewContext(context.Background())
	defer cancel()

	// This server simply shows the URL path as the page title, and contains
	// a link that points to /foo.
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")
		fmt.Fprintf(w, `
			<head><title>%s</title></head>
			<body><a id="foo" href="/foo">foo</a></body>
		`, r.URL.Path)
	}))
	defer ts.Close()

	// The Navigate action already waits until a page loads, so Title runs
	// once the page is ready.
	var firstTitle string
	if err := chromedp.Run(ctx,
		chromedp.Navigate(ts.URL),
		chromedp.Title(&firstTitle),
	); err != nil {
		log.Fatal(err)
	}
	fmt.Println("first title:", firstTitle)

	// However, actions like Click don't always trigger a page navigation,
	// so they don't wait for a page load directly. Wrapping them with
	// RunResponse does that waiting, and also obtains the HTTP response.
	resp, err := chromedp.RunResponse(ctx, chromedp.Click("#foo", chromedp.ByID))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("second status code:", resp.Status)

	// Grabbing the title again should work, as the page has finished
	// loading once more.
	var secondTitle string
	if err := chromedp.Run(ctx, chromedp.Title(&secondTitle)); err != nil {
		log.Fatal(err)
	}
	fmt.Println("second title:", secondTitle)

	// Finally, it's always possible to wrap Navigate with RunResponse, if
	// one wants the response information for that case too.
	resp, err = chromedp.RunResponse(ctx, chromedp.Navigate(ts.URL+"/bar"))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("third status code:", resp.Status)

	// Output:
	// first title: /
	// second status code: 200
	// second title: /foo
	// third status code: 200
}

func ExampleExecAllocator() {
	dir, err := os.MkdirTemp("", "chromedp-example")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)

	opts := append(chromedp.DefaultExecAllocatorOptions[:],
		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 {
		log.Fatal(err)
	}

	path := filepath.Join(dir, "DevToolsActivePort")
	bs, err := os.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}
	lines := bytes.Split(bs, []byte("\n"))
	fmt.Printf("DevToolsActivePort has %d lines\n", len(lines))

	// Output:
	// DevToolsActivePort has 2 lines
}

func ExampleNewContext_reuseBrowser() {
	ts := httptest.NewServer(writeHTML(`
<body>
<script>
	// Show the current cookies.
	var p = document.createElement("p")
	p.innerText = document.cookie
	p.setAttribute("id", "cookies")
	document.body.appendChild(p)

	// Override the cookies.
	document.cookie = "foo=bar"
</script>
</body>
	`))
	defer ts.Close()

	// create a new browser
	ctx, cancel := chromedp.NewContext(context.Background())
	defer cancel()

	// start the browser without a timeout
	if err := chromedp.Run(ctx); err != nil {
		log.Fatal(err)
	}

	for i := range 2 {
		func() {
			ctx, cancel := context.WithTimeout(ctx, time.Second)
			defer cancel()
			ctx, cancel = chromedp.NewContext(ctx)
			defer cancel()
			var cookies string
			if err := chromedp.Run(ctx,
				chromedp.Navigate(ts.URL),
				chromedp.Text("#cookies", &cookies),
			); err != nil {
				log.Fatal(err)
			}
			fmt.Printf("Cookies at i=%d: %q\n", i, cookies)
		}()
	}

	// Output:
	// Cookies at i=0: ""
	// Cookies at i=1: "foo=bar"
}

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 {
		log.Fatal(err)
	}

	// same browser, second tab
	ctx2, _ := chromedp.NewContext(ctx1)

	// ensure the second tab is created
	if err := chromedp.Run(ctx2); err != nil {
		log.Fatal(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
}

func ExampleListenTarget_consoleLog() {
	ctx, cancel := chromedp.NewContext(context.Background())
	
Download .txt
gitextract_oahvn68_/

├── .github/
│   ├── ISSUE_TEMPLATE
│   └── workflows/
│       └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── allocate.go
├── allocate_linux.go
├── allocate_other.go
├── allocate_test.go
├── browser.go
├── browser_test.go
├── call.go
├── chromedp.go
├── chromedp_test.go
├── conn.go
├── contrib/
│   └── docker-test.sh
├── device/
│   ├── device.go
│   └── gen.go
├── emulate.go
├── emulate_test.go
├── errors.go
├── eval.go
├── eval_test.go
├── event_test.go
├── example_test.go
├── go.mod
├── go.sum
├── input.go
├── input_test.go
├── js/
│   ├── attribute.js
│   ├── blur.js
│   ├── getClientRect.js
│   ├── reset.js
│   ├── setAttribute.js
│   ├── submit.js
│   ├── text.js
│   ├── textContent.js
│   ├── visible.js
│   └── waitForPredicatePageFunction.js
├── js.go
├── kb/
│   ├── gen.go
│   └── kb.go
├── nav.go
├── nav_test.go
├── poll.go
├── poll_test.go
├── query.go
├── query_test.go
├── screenshot.go
├── screenshot_test.go
├── target.go
├── testdata/
│   ├── alert.html
│   ├── child1.html
│   ├── child2.html
│   ├── consolespam.html
│   ├── dialog.html
│   ├── form.html
│   ├── frameset.html
│   ├── grid.html
│   ├── iframe.html
│   ├── image.html
│   ├── image2.html
│   ├── input.html
│   ├── js.html
│   ├── nested.html
│   ├── newtab.html
│   ├── poll.html
│   ├── screenshot.html
│   ├── svg.html
│   ├── table.html
│   ├── visible.html
│   └── webgl.html
├── util.go
└── util_test.go
Download .txt
SYMBOL INDEX (738 symbols across 44 files)

FILE: allocate.go
  type Allocator (line 23) | type Allocator interface
  function setupExecAllocator (line 37) | func setupExecAllocator(opts ...ExecAllocatorOption) *ExecAllocator {
  function NewExecAllocator (line 88) | func NewExecAllocator(parent context.Context, opts ...ExecAllocatorOptio...
  type ExecAllocator (line 105) | type ExecAllocator struct
    method Allocate (line 128) | func (a *ExecAllocator) Allocate(ctx context.Context, opts ...BrowserO...
    method Wait (line 325) | func (a *ExecAllocator) Wait() {
  function readOutput (line 285) | func readOutput(rc io.ReadCloser, forward io.Writer) (wsURL string, _ fu...
  function ExecPath (line 332) | func ExecPath(path string) ExecAllocatorOption {
  function findExecPath (line 348) | func findExecPath() string {
  function Flag (line 399) | func Flag(name string, value any) ExecAllocatorOption {
  function Env (line 408) | func Env(vars ...string) ExecAllocatorOption {
  function ModifyCmdFunc (line 418) | func ModifyCmdFunc(f func(cmd *exec.Cmd)) ExecAllocatorOption {
  function UserDataDir (line 429) | func UserDataDir(dir string) ExecAllocatorOption {
  function ProxyServer (line 434) | func ProxyServer(proxy string) ExecAllocatorOption {
  function IgnoreCertErrors (line 441) | func IgnoreCertErrors(a *ExecAllocator) {
  function WindowSize (line 446) | func WindowSize(width, height int) ExecAllocatorOption {
  function UserAgent (line 452) | func UserAgent(userAgent string) ExecAllocatorOption {
  function NoSandbox (line 457) | func NoSandbox(a *ExecAllocator) {
  function NoFirstRun (line 463) | func NoFirstRun(a *ExecAllocator) {
  function NoDefaultBrowserCheck (line 469) | func NoDefaultBrowserCheck(a *ExecAllocator) {
  function Headless (line 475) | func Headless(a *ExecAllocator) {
  function DisableGPU (line 496) | func DisableGPU(a *ExecAllocator) {
  function CombinedOutput (line 503) | func CombinedOutput(w io.Writer) ExecAllocatorOption {
  function WSURLReadTimeout (line 511) | func WSURLReadTimeout(t time.Duration) ExecAllocatorOption {
  function NewRemoteAllocator (line 532) | func NewRemoteAllocator(parent context.Context, url string, opts ...Remo...
  type RemoteAllocator (line 552) | type RemoteAllocator struct
    method Allocate (line 560) | func (a *RemoteAllocator) Allocate(ctx context.Context, opts ...Browse...
    method Wait (line 605) | func (a *RemoteAllocator) Wait() {
  function NoModifyURL (line 611) | func NoModifyURL(a *RemoteAllocator) {

FILE: allocate_linux.go
  function allocateCmdOptions (line 12) | func allocateCmdOptions(cmd *exec.Cmd) {

FILE: allocate_other.go
  function allocateCmdOptions (line 8) | func allocateCmdOptions(cmd *exec.Cmd) {

FILE: allocate_test.go
  function TestExecAllocator (line 19) | func TestExecAllocator(t *testing.T) {
  function TestExecAllocatorCancelParent (line 51) | func TestExecAllocatorCancelParent(t *testing.T) {
  function TestExecAllocatorCombinedOutputPanic (line 74) | func TestExecAllocatorCombinedOutputPanic(t *testing.T) {
  function TestExecAllocatorKillBrowser (line 105) | func TestExecAllocatorKillBrowser(t *testing.T) {
  function TestSkipNewContext (line 139) | func TestSkipNewContext(t *testing.T) {
  function TestRemoteAllocator (line 155) | func TestRemoteAllocator(t *testing.T) {
  function testRemoteAllocator (line 216) | func testRemoteAllocator(t *testing.T, modifyURL func(wsURL string) stri...
  function TestExecAllocatorMissingWebsocketAddr (line 331) | func TestExecAllocatorMissingWebsocketAddr(t *testing.T) {
  function TestCombinedOutput (line 353) | func TestCombinedOutput(t *testing.T) {
  function TestCombinedOutputError (line 381) | func TestCombinedOutputError(t *testing.T) {
  function TestEnv (line 405) | func TestEnv(t *testing.T) {
  function TestWithBrowserOptionAlreadyAllocated (line 430) | func TestWithBrowserOptionAlreadyAllocated(t *testing.T) {
  function TestModifyCmdFunc (line 449) | func TestModifyCmdFunc(t *testing.T) {
  function TestStartsWithNonBlankTab (line 485) | func TestStartsWithNonBlankTab(t *testing.T) {

FILE: browser.go
  type Browser (line 38) | type Browser struct
    method Process (line 147) | func (b *Browser) Process() *os.Process {
    method newExecutorForTarget (line 151) | func (b *Browser) newExecutorForTarget(ctx context.Context, targetID t...
    method Execute (line 182) | func (b *Browser) Execute(ctx context.Context, method string, params, ...
    method execute (line 190) | func (b *Browser) execute(ctx context.Context, method string, params, ...
    method run (line 243) | func (b *Browser) run(ctx context.Context) {
  function NewBrowser (line 94) | func NewBrowser(ctx context.Context, urlstr string, opts ...BrowserOptio...
  function WithBrowserLogf (line 349) | func WithBrowserLogf(f func(string, ...any)) BrowserOption {
  function WithBrowserErrorf (line 354) | func WithBrowserErrorf(f func(string, ...any)) BrowserOption {
  function WithBrowserDebugf (line 360) | func WithBrowserDebugf(f func(string, ...any)) BrowserOption {
  function WithConsolef (line 367) | func WithConsolef(f func(string, ...any)) BrowserOption {
  function WithDialTimeout (line 374) | func WithDialTimeout(d time.Duration) BrowserOption {

FILE: browser_test.go
  function TestUnmarshalWithDefaultOptions (line 11) | func TestUnmarshalWithDefaultOptions(t *testing.T) {
  function TestMarshalWithDefaultOptions (line 56) | func TestMarshalWithDefaultOptions(t *testing.T) {

FILE: call.go
  type CallAction (line 12) | type CallAction
  function CallFunctionOn (line 24) | func CallFunctionOn(functionDeclaration string, res any, opt CallOption,...
  function callFunctionOn (line 31) | func callFunctionOn(ctx context.Context, functionDeclaration string, res...
  type errAppender (line 78) | type errAppender struct
    method append (line 88) | func (ea *errAppender) append(v any) {

FILE: chromedp.go
  type Context (line 34) | type Context struct
    method newTarget (line 338) | func (c *Context) newTarget(ctx context.Context) error {
    method attachTarget (line 428) | func (c *Context) attachTarget(ctx context.Context, targetID target.ID...
  function NewContext (line 122) | func NewContext(parent context.Context, opts ...ContextOption) (context....
  type contextKey (line 223) | type contextKey struct
  function FromContext (line 226) | func FromContext(ctx context.Context) *Context {
  function Cancel (line 245) | func Cancel(ctx context.Context) error {
  function initContextBrowser (line 290) | func initContextBrowser(ctx context.Context) (*Context, error) {
  function Run (line 325) | func Run(ctx context.Context, actions ...Action) error {
  function WithTargetID (line 485) | func WithTargetID(id target.ID) ContextOption {
  function WithNewBrowserContext (line 496) | func WithNewBrowserContext(options ...CreateBrowserContextOption) Contex...
  function WithExistingBrowserContext (line 512) | func WithExistingBrowserContext(id cdp.BrowserContextID) ContextOption {
  function WithLogf (line 522) | func WithLogf(f func(string, ...any)) ContextOption {
  function WithErrorf (line 527) | func WithErrorf(f func(string, ...any)) ContextOption {
  function WithDebugf (line 532) | func WithDebugf(f func(string, ...any)) ContextOption {
  function WithBrowserOption (line 539) | func WithBrowserOption(opts ...BrowserOption) ContextOption {
  function RunResponse (line 558) | func RunResponse(ctx context.Context, actions ...Action) (*network.Respo...
  function responseAction (line 566) | func responseAction(resp **network.Response, actions ...Action) Action {
  function Targets (line 705) | func Targets(ctx context.Context) ([]*target.Info, error) {
  type Action (line 718) | type Action interface
  type ActionFunc (line 724) | type ActionFunc
    method Do (line 727) | func (f ActionFunc) Do(ctx context.Context) error {
  type Tasks (line 732) | type Tasks
    method Do (line 736) | func (t Tasks) Do(ctx context.Context) error {
  function Sleep (line 750) | func Sleep(d time.Duration) Action {
  function sleepContext (line 758) | func sleepContext(ctx context.Context, d time.Duration) error {
  function retryWithSleep (line 773) | func retryWithSleep(ctx context.Context, d time.Duration, f func(ctx con...
  type cancelableListener (line 786) | type cancelableListener struct
  function ListenBrowser (line 800) | func ListenBrowser(ctx context.Context, fn func(ev any)) {
  function ListenTarget (line 823) | func ListenTarget(ctx context.Context, fn func(ev any)) {
  function WaitNewTarget (line 841) | func WaitNewTarget(ctx context.Context, fn func(*target.Info) bool) <-ch...

FILE: chromedp_test.go
  function init (line 49) | func init() {
  function TestMain (line 84) | func TestMain(m *testing.M) {
  function testAllocate (line 107) | func testAllocate(tb testing.TB, name string) (context.Context, context....
  function testAllocateSeparate (line 135) | func testAllocateSeparate(tb testing.TB) (context.Context, context.Cance...
  function BenchmarkTabNavigate (line 154) | func BenchmarkTabNavigate(b *testing.B) {
  function checkTargets (line 184) | func checkTargets(tb testing.TB, ctx context.Context, want int) {
  function TestTargets (line 206) | func TestTargets(t *testing.T) {
  function TestCancelError (line 245) | func TestCancelError(t *testing.T) {
  function TestPrematureCancel (line 285) | func TestPrematureCancel(t *testing.T) {
  function TestPrematureCancelTab (line 298) | func TestPrematureCancelTab(t *testing.T) {
  function TestPrematureCancelAllocator (line 316) | func TestPrematureCancelAllocator(t *testing.T) {
  function TestConcurrentCancel (line 332) | func TestConcurrentCancel(t *testing.T) {
  function TestListenBrowser (line 357) | func TestListenBrowser(t *testing.T) {
  function TestListenTarget (line 394) | func TestListenTarget(t *testing.T) {
  function TestLargeEventCount (line 429) | func TestLargeEventCount(t *testing.T) {
  function TestLargeQuery (line 456) | func TestLargeQuery(t *testing.T) {
  function TestDialTimeout (line 486) | func TestDialTimeout(t *testing.T) {
  function TestListenCancel (line 529) | func TestListenCancel(t *testing.T) {
  function TestLogOptions (line 561) | func TestLogOptions(t *testing.T) {
  function TestBrowserContext (line 597) | func TestBrowserContext(t *testing.T) {
  function getBrowserContext (line 915) | func getBrowserContext(tb testing.TB, ctx context.Context) cdp.BrowserCo...
  function TestLargeOutboundMessages (line 929) | func TestLargeOutboundMessages(t *testing.T) {
  function TestDirectCloseTarget (line 943) | func TestDirectCloseTarget(t *testing.T) {
  function TestDirectCloseBrowser (line 964) | func TestDirectCloseBrowser(t *testing.T) {
  function TestDownloadIntoDir (line 983) | func TestDownloadIntoDir(t *testing.T) {
  function TestGracefulBrowserShutdown (line 1031) | func TestGracefulBrowserShutdown(t *testing.T) {
  function TestAttachingToWorkers (line 1083) | func TestAttachingToWorkers(t *testing.T) {
  function TestRunResponse (line 1148) | func TestRunResponse(t *testing.T) {
  function TestRunResponse_noResponse (line 1365) | func TestRunResponse_noResponse(t *testing.T) {
  function TestWebGL (line 1418) | func TestWebGL(t *testing.T) {
  function TestPDFTemplate (line 1465) | func TestPDFTemplate(t *testing.T) {
  function TestPDFBackground (line 1536) | func TestPDFBackground(t *testing.T) {

FILE: conn.go
  type Transport (line 20) | type Transport interface
  type Conn (line 27) | type Conn struct
    method Close (line 68) | func (c *Conn) Close() error {
    method Read (line 73) | func (c *Conn) Read(_ context.Context, msg *cdproto.Message) error {
    method Write (line 113) | func (c *Conn) Write(_ context.Context, msg *cdproto.Message) error {
  function DialContext (line 43) | func DialContext(ctx context.Context, urlstr string, opts ...DialOption)...
  function WithConnDebugf (line 148) | func WithConnDebugf(f func(string, ...any)) DialOption {

FILE: device/device.go
  type Info (line 12) | type Info struct
    method String (line 40) | func (i Info) String() string {
    method Device (line 45) | func (i Info) Device() Info {
  type infoType (line 50) | type infoType
    method String (line 53) | func (i infoType) String() string {
    method Device (line 58) | func (i infoType) Device() Info {
  constant Reset (line 65) | Reset infoType = iota
  constant BlackberryPlayBook (line 68) | BlackberryPlayBook
  constant BlackberryPlayBooklandscape (line 71) | BlackberryPlayBooklandscape
  constant BlackBerryZ30 (line 74) | BlackBerryZ30
  constant BlackBerryZ30landscape (line 77) | BlackBerryZ30landscape
  constant GalaxyNote3 (line 80) | GalaxyNote3
  constant GalaxyNote3landscape (line 83) | GalaxyNote3landscape
  constant GalaxyNoteII (line 86) | GalaxyNoteII
  constant GalaxyNoteIIlandscape (line 89) | GalaxyNoteIIlandscape
  constant GalaxySIII (line 92) | GalaxySIII
  constant GalaxySIIIlandscape (line 95) | GalaxySIIIlandscape
  constant GalaxyS5 (line 98) | GalaxyS5
  constant GalaxyS5landscape (line 101) | GalaxyS5landscape
  constant GalaxyS8 (line 104) | GalaxyS8
  constant GalaxyS8landscape (line 107) | GalaxyS8landscape
  constant GalaxyS9 (line 110) | GalaxyS9
  constant GalaxyS9landscape (line 113) | GalaxyS9landscape
  constant GalaxyTabS4 (line 116) | GalaxyTabS4
  constant GalaxyTabS4landscape (line 119) | GalaxyTabS4landscape
  constant IPad (line 122) | IPad
  constant IPadlandscape (line 125) | IPadlandscape
  constant IPadgen6 (line 128) | IPadgen6
  constant IPadgen6landscape (line 131) | IPadgen6landscape
  constant IPadgen7 (line 134) | IPadgen7
  constant IPadgen7landscape (line 137) | IPadgen7landscape
  constant IPadMini (line 140) | IPadMini
  constant IPadMinilandscape (line 143) | IPadMinilandscape
  constant IPadPro (line 146) | IPadPro
  constant IPadProlandscape (line 149) | IPadProlandscape
  constant IPadPro11 (line 152) | IPadPro11
  constant IPadPro11landscape (line 155) | IPadPro11landscape
  constant IPhone4 (line 158) | IPhone4
  constant IPhone4landscape (line 161) | IPhone4landscape
  constant IPhone5 (line 164) | IPhone5
  constant IPhone5landscape (line 167) | IPhone5landscape
  constant IPhone6 (line 170) | IPhone6
  constant IPhone6landscape (line 173) | IPhone6landscape
  constant IPhone6Plus (line 176) | IPhone6Plus
  constant IPhone6Pluslandscape (line 179) | IPhone6Pluslandscape
  constant IPhone7 (line 182) | IPhone7
  constant IPhone7landscape (line 185) | IPhone7landscape
  constant IPhone7Plus (line 188) | IPhone7Plus
  constant IPhone7Pluslandscape (line 191) | IPhone7Pluslandscape
  constant IPhone8 (line 194) | IPhone8
  constant IPhone8landscape (line 197) | IPhone8landscape
  constant IPhone8Plus (line 200) | IPhone8Plus
  constant IPhone8Pluslandscape (line 203) | IPhone8Pluslandscape
  constant IPhoneSE (line 206) | IPhoneSE
  constant IPhoneSElandscape (line 209) | IPhoneSElandscape
  constant IPhoneX (line 212) | IPhoneX
  constant IPhoneXlandscape (line 215) | IPhoneXlandscape
  constant IPhoneXR (line 218) | IPhoneXR
  constant IPhoneXRlandscape (line 221) | IPhoneXRlandscape
  constant IPhone11 (line 224) | IPhone11
  constant IPhone11landscape (line 227) | IPhone11landscape
  constant IPhone11Pro (line 230) | IPhone11Pro
  constant IPhone11Prolandscape (line 233) | IPhone11Prolandscape
  constant IPhone11ProMax (line 236) | IPhone11ProMax
  constant IPhone11ProMaxlandscape (line 239) | IPhone11ProMaxlandscape
  constant IPhone12 (line 242) | IPhone12
  constant IPhone12landscape (line 245) | IPhone12landscape
  constant IPhone12Pro (line 248) | IPhone12Pro
  constant IPhone12Prolandscape (line 251) | IPhone12Prolandscape
  constant IPhone12ProMax (line 254) | IPhone12ProMax
  constant IPhone12ProMaxlandscape (line 257) | IPhone12ProMaxlandscape
  constant IPhone12Mini (line 260) | IPhone12Mini
  constant IPhone12Minilandscape (line 263) | IPhone12Minilandscape
  constant IPhone13 (line 266) | IPhone13
  constant IPhone13landscape (line 269) | IPhone13landscape
  constant IPhone13Pro (line 272) | IPhone13Pro
  constant IPhone13Prolandscape (line 275) | IPhone13Prolandscape
  constant IPhone13ProMax (line 278) | IPhone13ProMax
  constant IPhone13ProMaxlandscape (line 281) | IPhone13ProMaxlandscape
  constant IPhone13Mini (line 284) | IPhone13Mini
  constant IPhone13Minilandscape (line 287) | IPhone13Minilandscape
  constant IPhone14 (line 290) | IPhone14
  constant IPhone14landscape (line 293) | IPhone14landscape
  constant IPhone14Plus (line 296) | IPhone14Plus
  constant IPhone14Pluslandscape (line 299) | IPhone14Pluslandscape
  constant IPhone14Pro (line 302) | IPhone14Pro
  constant IPhone14Prolandscape (line 305) | IPhone14Prolandscape
  constant IPhone14ProMax (line 308) | IPhone14ProMax
  constant IPhone14ProMaxlandscape (line 311) | IPhone14ProMaxlandscape
  constant IPhone15 (line 314) | IPhone15
  constant IPhone15landscape (line 317) | IPhone15landscape
  constant IPhone15Plus (line 320) | IPhone15Plus
  constant IPhone15Pluslandscape (line 323) | IPhone15Pluslandscape
  constant IPhone15Pro (line 326) | IPhone15Pro
  constant IPhone15Prolandscape (line 329) | IPhone15Prolandscape
  constant IPhone15ProMax (line 332) | IPhone15ProMax
  constant IPhone15ProMaxlandscape (line 335) | IPhone15ProMaxlandscape
  constant JioPhone2 (line 338) | JioPhone2
  constant JioPhone2landscape (line 341) | JioPhone2landscape
  constant KindleFireHDX (line 344) | KindleFireHDX
  constant KindleFireHDXlandscape (line 347) | KindleFireHDXlandscape
  constant LGOptimusL70 (line 350) | LGOptimusL70
  constant LGOptimusL70landscape (line 353) | LGOptimusL70landscape
  constant MicrosoftLumia550 (line 356) | MicrosoftLumia550
  constant MicrosoftLumia950 (line 359) | MicrosoftLumia950
  constant MicrosoftLumia950landscape (line 362) | MicrosoftLumia950landscape
  constant Nexus10 (line 365) | Nexus10
  constant Nexus10landscape (line 368) | Nexus10landscape
  constant Nexus4 (line 371) | Nexus4
  constant Nexus4landscape (line 374) | Nexus4landscape
  constant Nexus5 (line 377) | Nexus5
  constant Nexus5landscape (line 380) | Nexus5landscape
  constant Nexus5X (line 383) | Nexus5X
  constant Nexus5Xlandscape (line 386) | Nexus5Xlandscape
  constant Nexus6 (line 389) | Nexus6
  constant Nexus6landscape (line 392) | Nexus6landscape
  constant Nexus6P (line 395) | Nexus6P
  constant Nexus6Plandscape (line 398) | Nexus6Plandscape
  constant Nexus7 (line 401) | Nexus7
  constant Nexus7landscape (line 404) | Nexus7landscape
  constant NokiaLumia520 (line 407) | NokiaLumia520
  constant NokiaLumia520landscape (line 410) | NokiaLumia520landscape
  constant NokiaN9 (line 413) | NokiaN9
  constant NokiaN9landscape (line 416) | NokiaN9landscape
  constant Pixel2 (line 419) | Pixel2
  constant Pixel2landscape (line 422) | Pixel2landscape
  constant Pixel2XL (line 425) | Pixel2XL
  constant Pixel2XLlandscape (line 428) | Pixel2XLlandscape
  constant Pixel3 (line 431) | Pixel3
  constant Pixel3landscape (line 434) | Pixel3landscape
  constant Pixel4 (line 437) | Pixel4
  constant Pixel4landscape (line 440) | Pixel4landscape
  constant Pixel4a5G (line 443) | Pixel4a5G
  constant Pixel4a5Glandscape (line 446) | Pixel4a5Glandscape
  constant Pixel5 (line 449) | Pixel5
  constant Pixel5landscape (line 452) | Pixel5landscape
  constant MotoG4 (line 455) | MotoG4
  constant MotoG4landscape (line 458) | MotoG4landscape

FILE: device/gen.go
  constant deviceDescriptorsURL (line 20) | deviceDescriptorsURL = "https://raw.githubusercontent.com/puppeteer/pupp...
  function main (line 22) | func main() {
  type deviceDescriptor (line 31) | type deviceDescriptor struct
  function run (line 47) | func run(out string) error {
  function get (line 96) | func get() ([]deviceDescriptor, error) {
  constant hdr (line 131) | hdr = `// Package device contains device emulation definitions for use w...

FILE: emulate.go
  type EmulateAction (line 10) | type EmulateAction
  function EmulateViewport (line 21) | func EmulateViewport(width, height int64, opts ...EmulateViewportOption)...
  function EmulateScale (line 35) | func EmulateScale(scale float64) EmulateViewportOption {
  function EmulateOrientation (line 43) | func EmulateOrientation(orientation emulation.OrientationType, angle int...
  function EmulateLandscape (line 54) | func EmulateLandscape(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *...
  function EmulatePortrait (line 60) | func EmulatePortrait(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *e...
  function EmulateMobile (line 66) | func EmulateMobile(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *emu...
  function EmulateTouch (line 71) | func EmulateTouch(p1 *emulation.SetDeviceMetricsOverrideParams, p2 *emul...
  function ResetViewport (line 79) | func ResetViewport() EmulateAction {
  type Device (line 86) | type Device interface
  function Emulate (line 94) | func Emulate(device Device) EmulateAction {
  function EmulateReset (line 119) | func EmulateReset() EmulateAction {

FILE: emulate_test.go
  function TestEmulate (line 11) | func TestEmulate(t *testing.T) {

FILE: errors.go
  type Error (line 4) | type Error
    method Error (line 7) | func (err Error) Error() string {
  constant ErrInvalidWebsocketMessage (line 14) | ErrInvalidWebsocketMessage Error = "invalid websocket message"
  constant ErrInvalidDimensions (line 17) | ErrInvalidDimensions Error = "invalid dimensions"
  constant ErrNoResults (line 20) | ErrNoResults Error = "no results"
  constant ErrHasResults (line 23) | ErrHasResults Error = "has results"
  constant ErrNotVisible (line 26) | ErrNotVisible Error = "not visible"
  constant ErrVisible (line 29) | ErrVisible Error = "visible"
  constant ErrDisabled (line 32) | ErrDisabled Error = "disabled"
  constant ErrNotSelected (line 35) | ErrNotSelected Error = "not selected"
  constant ErrInvalidBoxModel (line 38) | ErrInvalidBoxModel Error = "invalid box model"
  constant ErrChannelClosed (line 41) | ErrChannelClosed Error = "channel closed"
  constant ErrInvalidTarget (line 44) | ErrInvalidTarget Error = "invalid target"
  constant ErrInvalidContext (line 47) | ErrInvalidContext Error = "invalid context"
  constant ErrPollingTimeout (line 50) | ErrPollingTimeout Error = "waiting for function failed: timeout"
  constant ErrJSUndefined (line 53) | ErrJSUndefined Error = "encountered an undefined value"
  constant ErrJSNull (line 56) | ErrJSNull Error = "encountered a null value"

FILE: eval.go
  type EvaluateAction (line 13) | type EvaluateAction
  function Evaluate (line 35) | func Evaluate(expression string, res any, opts ...EvaluateOption) Evalua...
  function parseRemoteObject (line 63) | func parseRemoteObject(v *runtime.RemoteObject, res any) (err error) {
  function EvaluateAsDevTools (line 111) | func EvaluateAsDevTools(expression string, res any, opts ...EvaluateOpti...
  function EvalObjectGroup (line 119) | func EvalObjectGroup(objectGroup string) EvaluateOption {
  function EvalWithCommandLineAPI (line 131) | func EvalWithCommandLineAPI(p *runtime.EvaluateParams) *runtime.Evaluate...
  function EvalIgnoreExceptions (line 137) | func EvalIgnoreExceptions(p *runtime.EvaluateParams) *runtime.EvaluatePa...
  function EvalAsValue (line 143) | func EvalAsValue(p *runtime.EvaluateParams) *runtime.EvaluateParams {

FILE: eval_test.go
  function TestEvaluateNumber (line 10) | func TestEvaluateNumber(t *testing.T) {
  function TestEvaluateString (line 51) | func TestEvaluateString(t *testing.T) {
  function TestEvaluateBytes (line 92) | func TestEvaluateBytes(t *testing.T) {
  function TestEvaluateRemoteObject (line 130) | func TestEvaluateRemoteObject(t *testing.T) {
  function TestEvaluateNil (line 173) | func TestEvaluateNil(t *testing.T) {

FILE: event_test.go
  function TestCloseDialog (line 11) | func TestCloseDialog(t *testing.T) {
  function TestWaitNewTarget (line 160) | func TestWaitNewTarget(t *testing.T) {

FILE: example_test.go
  function writeHTML (line 25) | func writeHTML(content string) http.Handler {
  function ExampleTitle (line 32) | func ExampleTitle() {
  function ExampleRunResponse (line 59) | func ExampleRunResponse() {
  function ExampleExecAllocator (line 117) | func ExampleExecAllocator() {
  function ExampleNewContext_reuseBrowser (line 153) | func ExampleNewContext_reuseBrowser() {
  function ExampleNewContext_manyTabs (line 201) | func ExampleNewContext_manyTabs() {
  function ExampleListenTarget_consoleLog (line 230) | func ExampleListenTarget_consoleLog() {
  function ExampleWaitNewTarget (line 282) | func ExampleWaitNewTarget() {
  function ExampleListenTarget_acceptAlert (line 317) | func ExampleListenTarget_acceptAlert() {
  function Example_retrieveHTML (line 352) | func Example_retrieveHTML() {
  function ExampleEmulate (line 389) | func ExampleEmulate() {
  function ExamplePrintToPDF (line 411) | func ExamplePrintToPDF() {
  function ExampleByJSPath (line 437) | func ExampleByJSPath() {
  function ExampleFromNode (line 472) | func ExampleFromNode() {
  function Example_dump (line 520) | func Example_dump() {
  function Example_documentDump (line 569) | func Example_documentDump() {
  function ExampleFullScreenshot (line 619) | func ExampleFullScreenshot() {
  function ExampleEvaluate (line 639) | func ExampleEvaluate() {

FILE: input.go
  type MouseAction (line 13) | type MouseAction
  function MouseEvent (line 17) | func MouseEvent(typ input.MouseType, x, y float64, opts ...MouseOption) ...
  function MouseClickXY (line 28) | func MouseClickXY(x, y float64, opts ...MouseOption) MouseAction {
  function MouseClickNode (line 57) | func MouseClickNode(n *cdp.Node, opts ...MouseOption) MouseAction {
  function Button (line 100) | func Button(btn string) MouseOption {
  function ButtonType (line 105) | func ButtonType(button input.MouseButton) MouseOption {
  function ButtonLeft (line 113) | func ButtonLeft(p *input.DispatchMouseEventParams) *input.DispatchMouseE...
  function ButtonMiddle (line 119) | func ButtonMiddle(p *input.DispatchMouseEventParams) *input.DispatchMous...
  function ButtonRight (line 125) | func ButtonRight(p *input.DispatchMouseEventParams) *input.DispatchMouse...
  function ButtonNone (line 131) | func ButtonNone(p *input.DispatchMouseEventParams) *input.DispatchMouseE...
  function ButtonModifiers (line 137) | func ButtonModifiers(modifiers ...input.Modifier) MouseOption {
  function ClickCount (line 147) | func ClickCount(n int) MouseOption {
  type KeyAction (line 154) | type KeyAction
  function KeyEvent (line 166) | func KeyEvent(keys string, opts ...KeyOption) KeyAction {
  function KeyEventNode (line 184) | func KeyEventNode(n *cdp.Node, keys string, opts ...KeyOption) KeyAction {
  function KeyModifiers (line 200) | func KeyModifiers(modifiers ...input.Modifier) KeyOption {

FILE: input_test.go
  constant inViewportJS (line 17) | inViewportJS = `(function(a) {
  function TestMouseClickXY (line 22) | func TestMouseClickXY(t *testing.T) {
  function TestMouseClickNode (line 70) | func TestMouseClickNode(t *testing.T) {
  function TestMouseClickOffscreenNode (line 117) | func TestMouseClickOffscreenNode(t *testing.T) {
  function TestKeyEvent (line 174) | func TestKeyEvent(t *testing.T) {
  function TestKeyEventNode (line 232) | func TestKeyEventNode(t *testing.T) {

FILE: js/attribute.js
  function attribute (line 1) | function attribute(n) {

FILE: js/blur.js
  function blur (line 1) | function blur() {

FILE: js/getClientRect.js
  function getClientRect (line 1) | function getClientRect() {

FILE: js/reset.js
  function reset (line 1) | function reset() {

FILE: js/setAttribute.js
  function setAttribute (line 1) | function setAttribute(n, v) {

FILE: js/submit.js
  function submit (line 1) | function submit() {

FILE: js/text.js
  function text (line 1) | function text() {

FILE: js/textContent.js
  function textContent (line 1) | function textContent() {

FILE: js/visible.js
  function visible (line 1) | function visible() {

FILE: js/waitForPredicatePageFunction.js
  function waitForPredicatePageFunction (line 1) | async function waitForPredicatePageFunction(predicateBody, polling, time...

FILE: kb/gen.go
  function main (line 22) | func main() {
  function run (line 31) | func run(out string) error {
  function loadKeys (line 55) | func loadKeys(keys map[rune]Key) error {
  function loadDomCodeData (line 101) | func loadDomCodeData() (map[string][]string, error) {
  function decodeRune (line 121) | func decodeRune(s string) rune {
  function getCode (line 145) | func getCode(s string) string {
  function addKey (line 154) | func addKey(keys map[rune]Key, r rune, key Key, scanCodeMap map[string][...
  function loadPrintable (line 172) | func loadPrintable(keys map[rune]Key, domCodeMap, domKeyMap map[string][...
  function loadDomKeyData (line 220) | func loadDomKeyData() (map[string][]string, error) {
  function loadNonPrintable (line 237) | func loadNonPrintable(keys map[rune]Key, domCodeMap, domKeyMap map[strin...
  function processKeys (line 272) | func processKeys(keys map[rune]Key) ([]byte, []byte, error) {
  function loadScanCodes (line 319) | func loadScanCodes(domCodeMap, domKeyMap map[string][]string, layoutBuf ...
  function loadPosixWinKeyboardCodes (line 350) | func loadPosixWinKeyboardCodes() (map[string][]int64, error) {
  function loadKeyboardCodes (line 392) | func loadKeyboardCodes(vkeyCodeMap map[string][]int64, lookup map[string...
  function extract (line 429) | func extract(buf []byte, name string) []byte {
  function grab (line 436) | func grab(path string) ([]byte, error) {
  constant chromiumSrc (line 467) | chromiumSrc = "https://chromium.googlesource.com/chromium/src/+/main/"
  constant domUsLayoutDataH (line 470) | domUsLayoutDataH = chromiumSrc + "ui/events/keycodes/dom_us_layout_data....
  constant domCodeDataInc (line 472) | domCodeDataInc = chromiumSrc + "ui/events/keycodes/dom/dom_code_data.inc...
  constant domKeyDataInc (line 474) | domKeyDataInc = chromiumSrc + "ui/events/keycodes/dom/dom_key_data.inc?f...
  constant keyboardCodesPosixH (line 476) | keyboardCodesPosixH = chromiumSrc + "ui/events/keycodes/keyboard_codes_p...
  constant keyboardCodesWinH (line 478) | keyboardCodesWinH = chromiumSrc + "ui/events/keycodes/keyboard_codes_win...
  constant windowsKeyboardCodesH (line 480) | windowsKeyboardCodesH = chromiumSrc + "third_party/blink/renderer/platfo...
  type Key (line 483) | type Key struct
  constant hdr (line 511) | hdr = `// Package kb provides keyboard mappings for Chrome DOM Keys for ...

FILE: kb/kb.go
  type Key (line 31) | type Key struct
  function EncodeUnidentified (line 53) | func EncodeUnidentified(r rune) []*input.DispatchKeyEventParams {
  function Encode (line 75) | func Encode(r rune) []*input.DispatchKeyEventParams {
  constant Backspace (line 120) | Backspace            = "\b"
  constant Tab (line 121) | Tab                  = "\t"
  constant Enter (line 122) | Enter                = "\r"
  constant Escape (line 123) | Escape               = "\u001b"
  constant Quote (line 124) | Quote                = "'"
  constant Backslash (line 125) | Backslash            = "\\"
  constant Delete (line 126) | Delete               = "\u007f"
  constant Alt (line 127) | Alt                  = "\u0102"
  constant CapsLock (line 128) | CapsLock             = "\u0104"
  constant Control (line 129) | Control              = "\u0105"
  constant Fn (line 130) | Fn                   = "\u0106"
  constant FnLock (line 131) | FnLock               = "\u0107"
  constant Hyper (line 132) | Hyper                = "\u0108"
  constant Meta (line 133) | Meta                 = "\u0109"
  constant NumLock (line 134) | NumLock              = "\u010a"
  constant ScrollLock (line 135) | ScrollLock           = "\u010c"
  constant Shift (line 136) | Shift                = "\u010d"
  constant Super (line 137) | Super                = "\u010e"
  constant ArrowDown (line 138) | ArrowDown            = "\u0301"
  constant ArrowLeft (line 139) | ArrowLeft            = "\u0302"
  constant ArrowRight (line 140) | ArrowRight           = "\u0303"
  constant ArrowUp (line 141) | ArrowUp              = "\u0304"
  constant End (line 142) | End                  = "\u0305"
  constant Home (line 143) | Home                 = "\u0306"
  constant PageDown (line 144) | PageDown             = "\u0307"
  constant PageUp (line 145) | PageUp               = "\u0308"
  constant Clear (line 146) | Clear                = "\u0401"
  constant Copy (line 147) | Copy                 = "\u0402"
  constant Cut (line 148) | Cut                  = "\u0404"
  constant Insert (line 149) | Insert               = "\u0407"
  constant Paste (line 150) | Paste                = "\u0408"
  constant Redo (line 151) | Redo                 = "\u0409"
  constant Undo (line 152) | Undo                 = "\u040a"
  constant Again (line 153) | Again                = "\u0502"
  constant Cancel (line 154) | Cancel               = "\u0504"
  constant ContextMenu (line 155) | ContextMenu          = "\u0505"
  constant Find (line 156) | Find                 = "\u0507"
  constant Help (line 157) | Help                 = "\u0508"
  constant Pause (line 158) | Pause                = "\u0509"
  constant Props (line 159) | Props                = "\u050b"
  constant Select (line 160) | Select               = "\u050c"
  constant ZoomIn (line 161) | ZoomIn               = "\u050d"
  constant ZoomOut (line 162) | ZoomOut              = "\u050e"
  constant BrightnessDown (line 163) | BrightnessDown       = "\u0601"
  constant BrightnessUp (line 164) | BrightnessUp         = "\u0602"
  constant Eject (line 165) | Eject                = "\u0604"
  constant LogOff (line 166) | LogOff               = "\u0605"
  constant Power (line 167) | Power                = "\u0606"
  constant PrintScreen (line 168) | PrintScreen          = "\u0608"
  constant WakeUp (line 169) | WakeUp               = "\u060b"
  constant Convert (line 170) | Convert              = "\u0705"
  constant ModeChange (line 171) | ModeChange           = "\u070b"
  constant NonConvert (line 172) | NonConvert           = "\u070d"
  constant HangulMode (line 173) | HangulMode           = "\u0711"
  constant HanjaMode (line 174) | HanjaMode            = "\u0712"
  constant Hiragana (line 175) | Hiragana             = "\u0716"
  constant KanaMode (line 176) | KanaMode             = "\u0718"
  constant Katakana (line 177) | Katakana             = "\u071a"
  constant ZenkakuHankaku (line 178) | ZenkakuHankaku       = "\u071d"
  constant F1 (line 179) | F1                   = "\u0801"
  constant F2 (line 180) | F2                   = "\u0802"
  constant F3 (line 181) | F3                   = "\u0803"
  constant F4 (line 182) | F4                   = "\u0804"
  constant F5 (line 183) | F5                   = "\u0805"
  constant F6 (line 184) | F6                   = "\u0806"
  constant F7 (line 185) | F7                   = "\u0807"
  constant F8 (line 186) | F8                   = "\u0808"
  constant F9 (line 187) | F9                   = "\u0809"
  constant F10 (line 188) | F10                  = "\u080a"
  constant F11 (line 189) | F11                  = "\u080b"
  constant F12 (line 190) | F12                  = "\u080c"
  constant F13 (line 191) | F13                  = "\u080d"
  constant F14 (line 192) | F14                  = "\u080e"
  constant F15 (line 193) | F15                  = "\u080f"
  constant F16 (line 194) | F16                  = "\u0810"
  constant F17 (line 195) | F17                  = "\u0811"
  constant F18 (line 196) | F18                  = "\u0812"
  constant F19 (line 197) | F19                  = "\u0813"
  constant F20 (line 198) | F20                  = "\u0814"
  constant F21 (line 199) | F21                  = "\u0815"
  constant F22 (line 200) | F22                  = "\u0816"
  constant F23 (line 201) | F23                  = "\u0817"
  constant F24 (line 202) | F24                  = "\u0818"
  constant Close (line 203) | Close                = "\u0a01"
  constant MailForward (line 204) | MailForward          = "\u0a02"
  constant MailReply (line 205) | MailReply            = "\u0a03"
  constant MailSend (line 206) | MailSend             = "\u0a04"
  constant MediaPlayPause (line 207) | MediaPlayPause       = "\u0a05"
  constant MediaStop (line 208) | MediaStop            = "\u0a07"
  constant MediaTrackNext (line 209) | MediaTrackNext       = "\u0a08"
  constant MediaTrackPrevious (line 210) | MediaTrackPrevious   = "\u0a09"
  constant New (line 211) | New                  = "\u0a0a"
  constant Open (line 212) | Open                 = "\u0a0b"
  constant Print (line 213) | Print                = "\u0a0c"
  constant Save (line 214) | Save                 = "\u0a0d"
  constant SpellCheck (line 215) | SpellCheck           = "\u0a0e"
  constant AudioVolumeDown (line 216) | AudioVolumeDown      = "\u0a0f"
  constant AudioVolumeUp (line 217) | AudioVolumeUp        = "\u0a10"
  constant AudioVolumeMute (line 218) | AudioVolumeMute      = "\u0a11"
  constant LaunchApplication2 (line 219) | LaunchApplication2   = "\u0b01"
  constant LaunchCalendar (line 220) | LaunchCalendar       = "\u0b02"
  constant LaunchMail (line 221) | LaunchMail           = "\u0b03"
  constant LaunchMediaPlayer (line 222) | LaunchMediaPlayer    = "\u0b04"
  constant LaunchMusicPlayer (line 223) | LaunchMusicPlayer    = "\u0b05"
  constant LaunchApplication1 (line 224) | LaunchApplication1   = "\u0b06"
  constant LaunchScreenSaver (line 225) | LaunchScreenSaver    = "\u0b07"
  constant LaunchSpreadsheet (line 226) | LaunchSpreadsheet    = "\u0b08"
  constant LaunchWebBrowser (line 227) | LaunchWebBrowser     = "\u0b09"
  constant LaunchContacts (line 228) | LaunchContacts       = "\u0b0c"
  constant LaunchPhone (line 229) | LaunchPhone          = "\u0b0d"
  constant LaunchAssistant (line 230) | LaunchAssistant      = "\u0b0e"
  constant BrowserBack (line 231) | BrowserBack          = "\u0c01"
  constant BrowserFavorites (line 232) | BrowserFavorites     = "\u0c02"
  constant BrowserForward (line 233) | BrowserForward       = "\u0c03"
  constant BrowserHome (line 234) | BrowserHome          = "\u0c04"
  constant BrowserRefresh (line 235) | BrowserRefresh       = "\u0c05"
  constant BrowserSearch (line 236) | BrowserSearch        = "\u0c06"
  constant BrowserStop (line 237) | BrowserStop          = "\u0c07"
  constant ChannelDown (line 238) | ChannelDown          = "\u0d0a"
  constant ChannelUp (line 239) | ChannelUp            = "\u0d0b"
  constant ClosedCaptionToggle (line 240) | ClosedCaptionToggle  = "\u0d12"
  constant Exit (line 241) | Exit                 = "\u0d15"
  constant Guide (line 242) | Guide                = "\u0d22"
  constant Info (line 243) | Info                 = "\u0d25"
  constant MediaFastForward (line 244) | MediaFastForward     = "\u0d2c"
  constant MediaLast (line 245) | MediaLast            = "\u0d2d"
  constant MediaPause (line 246) | MediaPause           = "\u0d2e"
  constant MediaPlay (line 247) | MediaPlay            = "\u0d2f"
  constant MediaRecord (line 248) | MediaRecord          = "\u0d30"
  constant MediaRewind (line 249) | MediaRewind          = "\u0d31"
  constant Settings (line 250) | Settings             = "\u0d43"
  constant ZoomToggle (line 251) | ZoomToggle           = "\u0d4e"
  constant AudioBassBoostToggle (line 252) | AudioBassBoostToggle = "\u0e02"
  constant SpeechInputToggle (line 253) | SpeechInputToggle    = "\u0f02"
  constant AppSwitch (line 254) | AppSwitch            = "\u1001"

FILE: nav.go
  type NavigateAction (line 16) | type NavigateAction
  function Navigate (line 19) | func Navigate(urlstr string) NavigateAction {
  function NavigationEntries (line 33) | func NavigationEntries(currentIndex *int64, entries *[]*page.NavigationE...
  function NavigateToHistoryEntry (line 47) | func NavigateToHistoryEntry(entryID int64) NavigateAction {
  function NavigateBack (line 53) | func NavigateBack() NavigateAction {
  function NavigateForward (line 71) | func NavigateForward() NavigateAction {
  function Reload (line 88) | func Reload() NavigateAction {
  function Stop (line 93) | func Stop() Action {
  function Location (line 98) | func Location(urlstr *string) Action {
  function Title (line 106) | func Title(title *string) Action {

FILE: nav_test.go
  function TestNavigate (line 20) | func TestNavigate(t *testing.T) {
  function TestNavigationEntries (line 42) | func TestNavigationEntries(t *testing.T) {
  function TestNavigateToHistoryEntry (line 88) | func TestNavigateToHistoryEntry(t *testing.T) {
  function TestNavigateBack (line 115) | func TestNavigateBack(t *testing.T) {
  function TestNavigateForward (line 138) | func TestNavigateForward(t *testing.T) {
  function TestStop (line 162) | func TestStop(t *testing.T) {
  function TestReload (line 172) | func TestReload(t *testing.T) {
  function TestLocation (line 209) | func TestLocation(t *testing.T) {
  function TestTitle (line 225) | func TestTitle(t *testing.T) {
  function TestQueryIframe (line 242) | func TestQueryIframe(t *testing.T) {
  function TestNavigateContextTimeout (line 273) | func TestNavigateContextTimeout(t *testing.T) {
  function writeHTML (line 292) | func writeHTML(content string) http.Handler {
  function TestNavigateWhileLoading (line 299) | func TestNavigateWhileLoading(t *testing.T) {
  function TestNavigateWithoutWaitingForLoad (line 373) | func TestNavigateWithoutWaitingForLoad(t *testing.T) {
  function TestNavigateCancelled (line 392) | func TestNavigateCancelled(t *testing.T) {

FILE: poll.go
  type PollAction (line 15) | type PollAction
  type pollTask (line 20) | type pollTask struct
    method Do (line 32) | func (p *pollTask) Do(ctx context.Context) error {
  function Poll (line 112) | func Poll(expression string, res any, opts ...PollOption) PollAction {
  function PollFunction (line 121) | func PollFunction(pageFunction string, res any, opts ...PollOption) Poll...
  function poll (line 127) | func poll(predicate string, res any, opts ...PollOption) PollAction {
  function WithPollingInterval (line 146) | func WithPollingInterval(interval time.Duration) PollOption {
  function WithPollingMutation (line 154) | func WithPollingMutation() PollOption {
  function WithPollingTimeout (line 163) | func WithPollingTimeout(timeout time.Duration) PollOption {
  function WithPollingInFrame (line 171) | func WithPollingInFrame(frame *cdp.Node) PollOption {
  function WithPollingArgs (line 178) | func WithPollingArgs(args ...any) PollOption {

FILE: poll_test.go
  function TestPoll (line 12) | func TestPoll(t *testing.T) {
  function TestPollFrame (line 178) | func TestPollFrame(t *testing.T) {
  function TestPollRemoteObject (line 201) | func TestPollRemoteObject(t *testing.T) {
  function TestPollTimeout (line 220) | func TestPollTimeout(t *testing.T) {

FILE: query.go
  type QueryAction (line 24) | type QueryAction
  type Selector (line 30) | type Selector struct
    method Do (line 157) | func (s *Selector) Do(ctx context.Context) error {
    method selAsString (line 225) | func (s *Selector) selAsString() string {
    method waitReady (line 233) | func (s *Selector) waitReady(check func(context.Context, runtime.Execu...
  function Query (line 132) | func Query(sel any, opts ...QueryOption) QueryAction {
  function QueryAfter (line 277) | func QueryAfter(sel any, f func(context.Context, runtime.ExecutionContex...
  function FromNode (line 290) | func FromNode(node *cdp.Node) QueryOption {
  function ByFunc (line 295) | func ByFunc(f func(context.Context, *cdp.Node) ([]cdp.NodeID, error)) Qu...
  function ByQuery (line 305) | func ByQuery(s *Selector) {
  function ByQueryAll (line 324) | func ByQueryAll(s *Selector) {
  function ByID (line 333) | func ByID(s *Selector) {
  function BySearch (line 340) | func BySearch(s *Selector) {
  function ByJSPath (line 372) | func ByJSPath(s *Selector) {
  function ByNodeID (line 408) | func ByNodeID(s *Selector) {
  function WaitFunc (line 427) | func WaitFunc(wait func(context.Context, *cdp.Frame, runtime.ExecutionCo...
  function NodeReady (line 435) | func NodeReady(s *Selector) {
  function callFunctionOnNode (line 439) | func callFunctionOnNode(ctx context.Context, node *cdp.Node, function st...
  function NodeVisible (line 464) | func NodeVisible(s *Selector) {
  function NodeNotVisible (line 491) | func NodeNotVisible(s *Selector) {
  function NodeEnabled (line 519) | func NodeEnabled(s *Selector) {
  function NodeSelected (line 537) | func NodeSelected(s *Selector) {
  function NodeNotPresent (line 556) | func NodeNotPresent(s *Selector) {
  function AtLeast (line 570) | func AtLeast(n int) QueryOption {
  function RetryInterval (line 580) | func RetryInterval(interval time.Duration) QueryOption {
  function After (line 589) | func After(f func(context.Context, runtime.ExecutionContextID, ...*cdp.N...
  function Populate (line 601) | func Populate(depth int64, pierce bool, opts ...PopulateOption) QueryOpt...
  function PopulateWait (line 627) | func PopulateWait(wait time.Duration) PopulateOption {
  function WaitReady (line 635) | func WaitReady(sel any, opts ...QueryOption) QueryAction {
  function WaitVisible (line 641) | func WaitVisible(sel any, opts ...QueryOption) QueryAction {
  function WaitNotVisible (line 647) | func WaitNotVisible(sel any, opts ...QueryOption) QueryAction {
  function WaitEnabled (line 653) | func WaitEnabled(sel any, opts ...QueryOption) QueryAction {
  function WaitSelected (line 659) | func WaitSelected(sel any, opts ...QueryOption) QueryAction {
  function WaitNotPresent (line 665) | func WaitNotPresent(sel any, opts ...QueryOption) QueryAction {
  function Nodes (line 671) | func Nodes(sel any, nodes *[]*cdp.Node, opts ...QueryOption) QueryAction {
  function NodeIDs (line 684) | func NodeIDs(sel any, ids *[]cdp.NodeID, opts ...QueryOption) QueryAction {
  function Focus (line 703) | func Focus(sel any, opts ...QueryOption) QueryAction {
  function Blur (line 715) | func Blur(sel any, opts ...QueryOption) QueryAction {
  function Dimensions (line 737) | func Dimensions(sel any, model **dom.BoxModel, opts ...QueryOption) Quer...
  function Text (line 753) | func Text(sel any, text *string, opts ...QueryOption) QueryAction {
  function TextContent (line 769) | func TextContent(sel any, text *string, opts ...QueryOption) QueryAction {
  function Clear (line 785) | func Clear(sel any, opts ...QueryOption) QueryAction {
  function Value (line 846) | func Value(sel any, value *string, opts ...QueryOption) QueryAction {
  function SetValue (line 859) | func SetValue(sel any, value string, opts ...QueryOption) QueryAction {
  function Attributes (line 865) | func Attributes(sel any, attributes *map[string]string, opts ...QueryOpt...
  function AttributesAll (line 894) | func AttributesAll(sel any, attributes *[]map[string]string, opts ...Que...
  function SetAttributes (line 920) | func SetAttributes(sel any, attributes map[string]string, opts ...QueryO...
  function AttributeValue (line 938) | func AttributeValue(sel any, name string, value *string, ok *bool, opts ...
  function SetAttributeValue (line 972) | func SetAttributeValue(sel any, name, value string, opts ...QueryOption)...
  function RemoveAttribute (line 984) | func RemoveAttribute(sel any, name string, opts ...QueryOption) QueryAct...
  function JavascriptAttribute (line 996) | func JavascriptAttribute(sel any, name string, res any, opts ...QueryOpt...
  function SetJavascriptAttribute (line 1015) | func SetJavascriptAttribute(sel any, name, value string, opts ...QueryOp...
  function OuterHTML (line 1036) | func OuterHTML(sel any, html *string, opts ...QueryOption) QueryAction {
  function InnerHTML (line 1045) | func InnerHTML(sel any, html *string, opts ...QueryOption) QueryAction {
  function Click (line 1054) | func Click(sel any, opts ...QueryOption) QueryAction {
  function DoubleClick (line 1066) | func DoubleClick(sel any, opts ...QueryOption) QueryAction {
  function SendKeys (line 1086) | func SendKeys(sel any, v string, opts ...QueryOption) QueryAction {
  function SetUploadFiles (line 1115) | func SetUploadFiles(sel any, files []string, opts ...QueryOption) QueryA...
  function Submit (line 1127) | func Submit(sel any, opts ...QueryOption) QueryAction {
  function Reset (line 1149) | func Reset(sel any, opts ...QueryOption) QueryAction {
  function ComputedStyle (line 1171) | func ComputedStyle(sel any, style *[]*css.ComputedStyleProperty, opts .....
  function MatchedStyle (line 1194) | func MatchedStyle(sel any, style **css.GetMatchedStylesForNodeReturns, o...
  function ScrollIntoView (line 1220) | func ScrollIntoView(sel any, opts ...QueryOption) QueryAction {
  function DumpTo (line 1235) | func DumpTo(sel any, w io.Writer, prefix, indent string, nodeIDs bool, d...
  function Dump (line 1255) | func Dump(sel any, w io.Writer, opts ...QueryOption) QueryAction {

FILE: query_test.go
  function TestWaitReady (line 25) | func TestWaitReady(t *testing.T) {
  function TestWaitVisible (line 47) | func TestWaitVisible(t *testing.T) {
  function TestWaitNotVisible (line 69) | func TestWaitNotVisible(t *testing.T) {
  function TestWaitEnabled (line 92) | func TestWaitEnabled(t *testing.T) {
  function TestWaitSelected (line 129) | func TestWaitSelected(t *testing.T) {
  function TestWaitNotPresent (line 163) | func TestWaitNotPresent(t *testing.T) {
  function TestAtLeast (line 178) | func TestAtLeast(t *testing.T) {
  function TestRetryInterval (line 193) | func TestRetryInterval(t *testing.T) {
  function TestNoRetryForInvalidSelector (line 254) | func TestNoRetryForInvalidSelector(t *testing.T) {
  function TestByJSPath (line 284) | func TestByJSPath(t *testing.T) {
  function TestNodes (line 308) | func TestNodes(t *testing.T) {
  function TestNodeIDs (line 338) | func TestNodeIDs(t *testing.T) {
  function TestFocusBlur (line 368) | func TestFocusBlur(t *testing.T) {
  function TestDimensions (line 414) | func TestDimensions(t *testing.T) {
  function TestText (line 445) | func TestText(t *testing.T) {
  function TestTextContent (line 477) | func TestTextContent(t *testing.T) {
  function TestClear (line 504) | func TestClear(t *testing.T) {
  function TestReset (line 556) | func TestReset(t *testing.T) {
  function TestValue (line 595) | func TestValue(t *testing.T) {
  function TestValueUndefined (line 624) | func TestValueUndefined(t *testing.T) {
  function TestSetValue (line 639) | func TestSetValue(t *testing.T) {
  function TestAttributes (line 689) | func TestAttributes(t *testing.T) {
  function TestAttributesAll (line 754) | func TestAttributesAll(t *testing.T) {
  function TestSetAttributes (line 794) | func TestSetAttributes(t *testing.T) {
  function TestAttributeValue (line 884) | func TestAttributeValue(t *testing.T) {
  function TestSetAttributeValue (line 918) | func TestSetAttributeValue(t *testing.T) {
  function TestRemoveAttribute (line 963) | func TestRemoveAttribute(t *testing.T) {
  function TestClick (line 1004) | func TestClick(t *testing.T) {
  function TestDoubleClick (line 1041) | func TestDoubleClick(t *testing.T) {
  function TestSendKeys (line 1077) | func TestSendKeys(t *testing.T) {
  function TestSubmit (line 1121) | func TestSubmit(t *testing.T) {
  function TestComputedStyle (line 1158) | func TestComputedStyle(t *testing.T) {
  function TestMatchedStyle (line 1209) | func TestMatchedStyle(t *testing.T) {
  function TestFileUpload (line 1240) | func TestFileUpload(t *testing.T) {
  function TestInnerHTML (line 1306) | func TestInnerHTML(t *testing.T) {
  function TestOuterHTML (line 1333) | func TestOuterHTML(t *testing.T) {
  function TestScrollIntoView (line 1360) | func TestScrollIntoView(t *testing.T) {
  function TestSVGFullXPath (line 1385) | func TestSVGFullXPath(t *testing.T) {
  constant uploadHTML (line 1439) | uploadHTML = `<!doctype html>
  constant resultHTML (line 1449) | resultHTML = `<!doctype html>
  function TestWaitReadyReuseAction (line 1457) | func TestWaitReadyReuseAction(t *testing.T) {
  function TestFromNode (line 1472) | func TestFromNode(t *testing.T) {

FILE: screenshot.go
  function Screenshot (line 32) | func Screenshot(sel any, picbuf *[]byte, opts ...QueryOption) QueryAction {
  function ScreenshotScale (line 38) | func ScreenshotScale(sel any, scale float64, picbuf *[]byte, opts ...Que...
  function ScreenshotNodes (line 54) | func ScreenshotNodes(nodes []*cdp.Node, scale float64, picbuf *[]byte) A...
  function CaptureScreenshot (line 117) | func CaptureScreenshot(res *[]byte) Action {
  function FullScreenshot (line 139) | func FullScreenshot(res *[]byte, quality int) EmulateAction {
  function extents (line 164) | func extents(m, n, o, p float64) (float64, float64) {

FILE: screenshot_test.go
  function TestScreenshot (line 16) | func TestScreenshot(t *testing.T) {
  function TestScreenshotScale (line 81) | func TestScreenshotScale(t *testing.T) {
  function TestScreenshotHighDPI (line 127) | func TestScreenshotHighDPI(t *testing.T) {
  function TestCaptureScreenshot (line 156) | func TestCaptureScreenshot(t *testing.T) {
  function TestFullScreenshot (line 179) | func TestFullScreenshot(t *testing.T) {
  function matchPixel (line 225) | func matchPixel(buf []byte, want string) (int, error) {
  function openImage (line 243) | func openImage(screenshot string) (image.Image, string, error) {

FILE: target.go
  type Target (line 20) | type Target struct
    method enclosingFrame (line 45) | func (t *Target) enclosingFrame(node *cdp.Node) cdp.FrameID {
    method ensureFrame (line 68) | func (t *Target) ensureFrame() (*cdp.Frame, *cdp.Node, runtime.Executi...
    method run (line 90) | func (t *Target) run(ctx context.Context) {
    method Execute (line 161) | func (t *Target) Execute(ctx context.Context, method string, params, r...
    method runtimeEvent (line 220) | func (t *Target) runtimeEvent(ev any) {
    method documentUpdated (line 257) | func (t *Target) documentUpdated(ctx context.Context) {
    method pageEvent (line 290) | func (t *Target) pageEvent(ev any) {
    method domEvent (line 362) | func (t *Target) domEvent(ctx context.Context, ev any) {

FILE: util.go
  function forceIP (line 22) | func forceIP(ctx context.Context, urlstr string) (string, error) {
  function resolveHost (line 41) | func resolveHost(ctx context.Context, host string) (string, error) {
  function modifyURL (line 71) | func modifyURL(ctx context.Context, urlstr string) (string, error) {
  function runListeners (line 123) | func runListeners(list []cancelableListener, ev any) []cancelableListener {
  type frameOp (line 139) | type frameOp
  function frameAttached (line 141) | func frameAttached(id cdp.FrameID) frameOp {
  function frameDetached (line 148) | func frameDetached(f *cdp.Frame) {
  function frameStartedLoading (line 153) | func frameStartedLoading(f *cdp.Frame) {
  function frameStoppedLoading (line 157) | func frameStoppedLoading(f *cdp.Frame) {
  function setFrameState (line 162) | func setFrameState(f *cdp.Frame, fs cdp.FrameState) {
  function clearFrameState (line 167) | func clearFrameState(f *cdp.Frame, fs cdp.FrameState) {
  type nodeOp (line 172) | type nodeOp
  function walk (line 174) | func walk(m map[cdp.NodeID]*cdp.Node, n *cdp.Node) {
  function setChildNodes (line 220) | func setChildNodes(m map[cdp.NodeID]*cdp.Node, nodes []*cdp.Node) nodeOp {
  function attributeModified (line 230) | func attributeModified(name, value string) nodeOp {
  function attributeRemoved (line 253) | func attributeRemoved(name string) nodeOp {
  function inlineStyleInvalidated (line 269) | func inlineStyleInvalidated(ids []cdp.NodeID) nodeOp {
  function characterDataModified (line 274) | func characterDataModified(characterData string) nodeOp {
  function childNodeCountUpdated (line 283) | func childNodeCountUpdated(count int64) nodeOp {
  function childNodeInserted (line 292) | func childNodeInserted(m map[cdp.NodeID]*cdp.Node, prevID cdp.NodeID, c ...
  function childNodeRemoved (line 302) | func childNodeRemoved(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) nodeOp {
  function shadowRootPushed (line 312) | func shadowRootPushed(m map[cdp.NodeID]*cdp.Node, c *cdp.Node) nodeOp {
  function shadowRootPopped (line 322) | func shadowRootPopped(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) nodeOp {
  function pseudoElementAdded (line 332) | func pseudoElementAdded(m map[cdp.NodeID]*cdp.Node, c *cdp.Node) nodeOp {
  function pseudoElementRemoved (line 342) | func pseudoElementRemoved(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) nod...
  function distributedNodesUpdated (line 352) | func distributedNodesUpdated(nodes []*cdp.BackendNode) nodeOp {
  function scrollableFlagUpdated (line 361) | func scrollableFlagUpdated(m map[cdp.NodeID]*cdp.Node, id cdp.NodeID) no...
  function insertNode (line 366) | func insertNode(n []*cdp.Node, prevID cdp.NodeID, c *cdp.Node) []*cdp.No...
  function removeNode (line 388) | func removeNode(n []*cdp.Node, id cdp.NodeID) []*cdp.Node {
  function isCouldNotComputeBoxModelError (line 411) | func isCouldNotComputeBoxModelError(err error) bool {

FILE: util_test.go
  constant dumpJS (line 24) | dumpJS = `(function dump(n, prefix, indent, nodeIDs) {
  constant insertJS (line 109) | insertJS = `(function(n, typ, id, text) {
  function TestNodeOp (line 124) | func TestNodeOp(t *testing.T) {
Condensed preview — 74 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (471K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE",
    "chars": 612,
    "preview": "<!---\nThis issue tracker is mainly for bugs and feature requests. Before asking a\nquestion, search online and try to inv"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 596,
    "preview": "on: [push, pull_request]\nname: Test\njobs:\n  test:\n    strategy:\n      matrix:\n        go-version: [oldstable, stable]\n  "
  },
  {
    "path": ".gitignore",
    "chars": 110,
    "preview": "out.txt\nout*.txt\nold*.txt\ncdp-*.log\ncdp-*.txt\n*.out\n\n/chromedp.test\n/chromedp.test.exe\n\n/*.jpeg\n/*.png\n/*.pdf\n"
  },
  {
    "path": "LICENSE",
    "chars": 1084,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016-2025 Kenneth Shaw\n\nPermission is hereby granted, free of charge, to any person"
  },
  {
    "path": "README.md",
    "chars": 4178,
    "preview": "# About chromedp\n\nPackage `chromedp` is a faster, simpler way to drive browsers supporting the\n[Chrome DevTools Protocol"
  },
  {
    "path": "allocate.go",
    "chars": 18817,
    "preview": "package chromedp\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runti"
  },
  {
    "path": "allocate_linux.go",
    "chars": 414,
    "preview": "//go:build linux\n// +build linux\n\npackage chromedp\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc allocateCmdOptions(cmd "
  },
  {
    "path": "allocate_other.go",
    "chars": 115,
    "preview": "//go:build !linux\n// +build !linux\n\npackage chromedp\n\nimport \"os/exec\"\n\nfunc allocateCmdOptions(cmd *exec.Cmd) {\n}\n"
  },
  {
    "path": "allocate_test.go",
    "chars": 13426,
    "preview": "package chromedp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\""
  },
  {
    "path": "browser.go",
    "chars": 10264,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/chromed"
  },
  {
    "path": "browser_test.go",
    "chars": 3630,
    "preview": "package chromedp\n\nimport (\n\t\"bytes\"\n\t\"github.com/chromedp/cdproto\"\n\tjsonv2 \"github.com/go-json-experiment/json\"\n\t\"github"
  },
  {
    "path": "call.go",
    "chars": 2549,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\n\t\"github.com/chromedp/cdproto/runtime\"\n\tjsonv2 \"github.com/go-json-experiment/jso"
  },
  {
    "path": "chromedp.go",
    "chars": 27893,
    "preview": "// Package chromedp is a high level Chrome DevTools Protocol client that\n// simplifies driving browsers for scraping, un"
  },
  {
    "path": "chromedp_test.go",
    "chars": 42066,
    "preview": "package chromedp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"image/color\"\n\t\"image/png\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/ht"
  },
  {
    "path": "conn.go",
    "chars": 3975,
    "preview": "package chromedp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"net\"\n\n\t\"github.com/chromedp/cdproto\"\n\tjsonv2 \"github.com/go-json-"
  },
  {
    "path": "contrib/docker-test.sh",
    "chars": 546,
    "preview": "#!/bin/bash\n\nSRC=$(realpath $(cd -P \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/..)\n\npushd $SRC &> /dev/null\n\nIMAGE=${IMAGE"
  },
  {
    "path": "device/device.go",
    "chars": 36722,
    "preview": "// Package device contains device emulation definitions for use with chromedp's\n// Emulate action.\n//\n// See: https://ra"
  },
  {
    "path": "device/gen.go",
    "chars": 4809,
    "preview": "//go:build ignore\n// +build ignore\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"go/forma"
  },
  {
    "path": "emulate.go",
    "chars": 4432,
    "preview": "package chromedp\n\nimport (\n\t\"github.com/chromedp/cdproto/emulation\"\n\t\"github.com/chromedp/chromedp/device\"\n)\n\n// Emulate"
  },
  {
    "path": "emulate_test.go",
    "chars": 570,
    "preview": "package chromedp\n\nimport (\n\t\"bytes\"\n\t\"image/png\"\n\t\"testing\"\n\n\t\"github.com/chromedp/chromedp/device\"\n)\n\nfunc TestEmulate("
  },
  {
    "path": "errors.go",
    "chars": 1712,
    "preview": "package chromedp\n\n// Error is a chromedp error.\ntype Error string\n\n// Error satisfies the error interface.\nfunc (err Err"
  },
  {
    "path": "eval.go",
    "chars": 4589,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"reflect\"\n\n\t\"github.com/chromedp/cdproto/runtime\"\n)\n\n// Evaluate"
  },
  {
    "path": "eval_test.go",
    "chars": 3944,
    "preview": "package chromedp\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/chromedp/cdproto/runtime\"\n)\n\nfunc TestEvaluateNumber(t *t"
  },
  {
    "path": "event_test.go",
    "chars": 4425,
    "preview": "package chromedp\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/chromedp/cdproto/page\"\n\t\"github.com/chromedp/cdproto/targ"
  },
  {
    "path": "example_test.go",
    "chars": 17922,
    "preview": "package chromedp_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/fi"
  },
  {
    "path": "go.mod",
    "chars": 541,
    "preview": "module github.com/chromedp/chromedp\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266"
  },
  {
    "path": "go.sum",
    "chars": 1828,
    "preview": "github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=\ngithub.co"
  },
  {
    "path": "input.go",
    "chars": 5643,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\n\t\"github.com/chromedp/cdproto/cdp\"\n\t\"github.com/chromedp/cdproto/dom\"\n\t\"github.co"
  },
  {
    "path": "input_test.go",
    "chars": 7344,
    "preview": "package chromedp\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/chromedp/cdproto/cdp\"\n\t\"github.com/chromedp/"
  },
  {
    "path": "js/attribute.js",
    "chars": 46,
    "preview": "function attribute(n) {\n    return this[n];\n}\n"
  },
  {
    "path": "js/blur.js",
    "chars": 54,
    "preview": "function blur() {\n    this.blur();\n    return true;\n}\n"
  },
  {
    "path": "js/getClientRect.js",
    "chars": 243,
    "preview": "function getClientRect() {\n  const e = this.getBoundingClientRect(),\n    t = this.ownerDocument.documentElement.getBound"
  },
  {
    "path": "js/reset.js",
    "chars": 269,
    "preview": "function reset() {\n    if (this.nodeName === 'FORM') {\n        HTMLFormElement.prototype.reset.call(this);\n        retur"
  },
  {
    "path": "js/setAttribute.js",
    "chars": 231,
    "preview": "function setAttribute(n, v) {\n    this[n] = v;\n    if (n === 'value') {\n        this.dispatchEvent(new Event('input', {b"
  },
  {
    "path": "js/submit.js",
    "chars": 272,
    "preview": "function submit() {\n    if (this.nodeName === 'FORM') {\n        HTMLFormElement.prototype.submit.call(this);\n        ret"
  },
  {
    "path": "js/text.js",
    "chars": 153,
    "preview": "function text() {\n    if (this.offsetWidth || this.offsetHeight || this.getClientRects().length) {\n        return this.i"
  },
  {
    "path": "js/textContent.js",
    "chars": 56,
    "preview": "function textContent() {\n    return this.textContent;\n}\n"
  },
  {
    "path": "js/visible.js",
    "chars": 114,
    "preview": "function visible() {\n    return Boolean(this.offsetWidth || this.offsetHeight || this.getClientRects().length);\n}\n"
  },
  {
    "path": "js/waitForPredicatePageFunction.js",
    "chars": 2375,
    "preview": "async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {\n    const predicate = new Functi"
  },
  {
    "path": "js.go",
    "chars": 2075,
    "preview": "package chromedp\n\nimport (\n\t_ \"embed\"\n)\n\nvar (\n\t// textJS is a JavaScript snippet that returns the innerText of the spec"
  },
  {
    "path": "kb/gen.go",
    "chars": 18395,
    "preview": "//go:build ignore\n// +build ignore\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"go/for"
  },
  {
    "path": "kb/kb.go",
    "chars": 23642,
    "preview": "// Package kb provides keyboard mappings for Chrome DOM Keys for use with input\n// events.\npackage kb\n\n// Code generated"
  },
  {
    "path": "nav.go",
    "chars": 3038,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/chromedp/cdproto/page\"\n)\n\n// NavigateAction are act"
  },
  {
    "path": "nav_test.go",
    "chars": 10029,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t_ \"image/png\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n"
  },
  {
    "path": "poll.go",
    "chars": 5577,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/chromedp/cdproto/cdp\"\n\t\"github.com/chromedp/cdproto/r"
  },
  {
    "path": "poll_test.go",
    "chars": 5888,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/chromedp/cdproto/cdp\"\n\t\"github.com/chromedp/cdpro"
  },
  {
    "path": "query.go",
    "chars": 39331,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/chromed"
  },
  {
    "path": "query_test.go",
    "chars": 35906,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"r"
  },
  {
    "path": "screenshot.go",
    "chars": 5180,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\n\t\"github.com/chromedp/cdproto/cdp\"\n\t\"github.com/chromedp/cdproto/p"
  },
  {
    "path": "screenshot_test.go",
    "chars": 5192,
    "preview": "package chromedp\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"image\"\n\t_ \"image/jpeg\"\n\t_ \"image/png\"\n\t\"os\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.co"
  },
  {
    "path": "target.go",
    "chars": 10758,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/chromedp/cdproto\"\n"
  },
  {
    "path": "testdata/alert.html",
    "chars": 359,
    "preview": "<!doctype html>\n<html>\n<head>\n<title>javascript alert test</title>\n</head>\n<body>\n  <div id=\"div1\">\n    <input id=\"input"
  },
  {
    "path": "testdata/child1.html",
    "chars": 118,
    "preview": "<html>\n<head>\n<title>child 1</title>\n</head>\n<body>\n  <div id=\"child1\">\n    <p>child one</p>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "testdata/child2.html",
    "chars": 118,
    "preview": "<html>\n<head>\n<title>child 2</title>\n</head>\n<body>\n  <div id=\"child2\">\n    <p>child two</p>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "testdata/consolespam.html",
    "chars": 250,
    "preview": "<!doctype html>\n<html>\n<body>\n  <script>\n    for (var i = 0; i < 2000; i++) {\n      console.log(\"spam\", i)\n    }\n    var"
  },
  {
    "path": "testdata/dialog.html",
    "chars": 381,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <title>chromedp dialog</title>\n  </head>\n  <body>\n    <input id='alert' type='button"
  },
  {
    "path": "testdata/form.html",
    "chars": 1258,
    "preview": "<!doctype html>\n<html>\n<head>\n  <title>this is form title</title>\n  <style>\n    input, textarea {\n      margin-top: 5px\n"
  },
  {
    "path": "testdata/frameset.html",
    "chars": 153,
    "preview": "<html>\n<head>\n  <title>frameset test</title>\n</head>\n<frameset cols=\"50%,*\">\n  <frame src=\"child1.html\">\n  <frame src=\"c"
  },
  {
    "path": "testdata/grid.html",
    "chars": 1211,
    "preview": "<script>\ndocument.addEventListener('DOMContentLoaded', function() {\n    function generatePalette(amount) {\n        var r"
  },
  {
    "path": "testdata/iframe.html",
    "chars": 160,
    "preview": "<!doctype html>\n<html>\n<head>\n  <title>page with an iframe</title>\n</head>\n<body>\n  <div id=\"parent\"></div>\n  <iframe sr"
  },
  {
    "path": "testdata/image.html",
    "chars": 589,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <title>this is title</title>\n    <style>\n      #half-color {\n        display: inline"
  },
  {
    "path": "testdata/image2.html",
    "chars": 1099,
    "preview": "<!doctype html>\n<html>\n<head>\n  <title>this is title</title>\n  <style>\n    #half-color {\n      display: inline-block;\n  "
  },
  {
    "path": "testdata/input.html",
    "chars": 1849,
    "preview": "<!doctype html>\n<html>\n<body style=\"background-color: white;\">\n  <script>\n    window.document.test_i = 0\n  </script>\n  <"
  },
  {
    "path": "testdata/js.html",
    "chars": 1577,
    "preview": "<!doctype html>\n<html>\n<head>\n  <style>\n    body {\n        background-color: antiquewhite;\n    }\n    input[type=\"number\""
  },
  {
    "path": "testdata/nested.html",
    "chars": 433,
    "preview": "<!doctype html>\n<html>\n<head>\n  <title>page with nested elements</title>\n</head>\n<body>\n  <p class=\"content\">body root c"
  },
  {
    "path": "testdata/newtab.html",
    "chars": 225,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <title>chromedp tab test page</title>\n  </head>\n  <body>\n    <input id='new-tab' typ"
  },
  {
    "path": "testdata/poll.html",
    "chars": 437,
    "preview": "<!doctype html>\n<html>\n<head>\n    <title>chromedp poll test page</title>\n    <script>\n        let timeout = 10;\n        "
  },
  {
    "path": "testdata/screenshot.html",
    "chars": 1864,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <title>screenshot</title>\n    <style>\n        #p"
  },
  {
    "path": "testdata/svg.html",
    "chars": 269,
    "preview": "<!doctype html>\n<html>\n<body>\n<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox"
  },
  {
    "path": "testdata/table.html",
    "chars": 539,
    "preview": "<!doctype html>\n<html>\n<head>\n  <title>table</title>\n</head>\n<body>\n  <table>\n    <thead>\n      <tr>\n        <th>row 1 h"
  },
  {
    "path": "testdata/visible.html",
    "chars": 672,
    "preview": "<!doctype html>\n<html>\n<head>\n  <title>example</title>\n</head>\n<body>\n  <div id=\"box1\" style=\"display:none\">\n    <div id"
  },
  {
    "path": "testdata/webgl.html",
    "chars": 6015,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf8\" />\n    <!--\n    http://twgljs.org/examples/twgl-cube.html\n\n    "
  },
  {
    "path": "util.go",
    "chars": 8772,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ch"
  },
  {
    "path": "util_test.go",
    "chars": 4829,
    "preview": "package chromedp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.co"
  }
]

About this extraction

This page contains the full source code of the chromedp/chromedp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 74 files (416.8 KB), approximately 127.6k tokens, and a symbol index with 738 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!