pax_global_header 0000666 0000000 0000000 00000000064 15206741465 0014524 g ustar 00root root 0000000 0000000 52 comment=872e0c840c7e15f297487dcbf49bb0779f188a82
golang-github-go-rod-rod-0.116.2/ 0000775 0000000 0000000 00000000000 15206741465 0016371 5 ustar 00root root 0000000 0000000 golang-github-go-rod-rod-0.116.2/.eslintrc.yml 0000664 0000000 0000000 00000000166 15206741465 0021020 0 ustar 00root root 0000000 0000000 extends:
- eslint:recommended
env:
browser: true
es6: true
parserOptions:
ecmaVersion: 2018
plugins:
- html
golang-github-go-rod-rod-0.116.2/.github/ 0000775 0000000 0000000 00000000000 15206741465 0017731 5 ustar 00root root 0000000 0000000 golang-github-go-rod-rod-0.116.2/.github/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000006407 15206741465 0022537 0 ustar 00root root 0000000 0000000 # Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at . All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
golang-github-go-rod-rod-0.116.2/.github/CONTRIBUTING.md 0000664 0000000 0000000 00000012253 15206741465 0022165 0 ustar 00root root 0000000 0000000 # Contributing
Anyone has contributed code to the project can become a member of the project and have the write permission to issues and doc repositories.
At the early stage of this project, we will use a simple model to promote members to maintainers.
Maintainers will have all the permissions of this project, only the first 2 maintainers are granted by the owner, the standard is whether the member is good enough to review others' code, then we will start to elect
new maintainers by voting in the public issue. If no one votes down and 2/3 votes up then an election passes.
## Contribute Doc
Check [here](https://github.com/go-rod/go-rod.github.io/blob/main/contribute-doc.md).
## Terminology
When we talk about type in the doc we use [gopls](https://github.com/golang/tools/tree/master/gopls) symbol query syntax. For example, when we say `rod.Page.PDF`, you can run the below to locate the file and line of it:
```bash
gopls workspace_symbol -matcher=fuzzy rod.Page.PDF$
```
- `cdp`: It's short for Chrome Devtools Protocol
## How it works
Here's the common start process of rod:
1. Try to connect to a Devtools endpoint (WebSocket), if not found try to launch a local browser, if still not found try to download one, then connect again. The lib to handle it is [launcher](lib/launcher).
1. Use the JSON-RPC to talk to the Devtools endpoint to control the browser. The lib handles it is [cdp](lib/cdp).
1. Use the type definitions of the JSON-RPC to perform high-level actions. The lib handles it is [proto](lib/proto).
Object model:

## Run tests
First, launch a test shell for rod:
```bash
go run ./lib/utils/shell
```
Then, no magic, just `go test`. Read the test template [rod_test.go](../rod_test.go) to get started.
The entry point of tests is [setup_test.go](../setup_test.go). All the test helpers are defined in it.
The `cdp` requests of each test will be recorded and output to folder `tmp/cdp-log`, the CI will store them as
[artifacts](https://docs.github.com/en/actions/guides/storing-workflow-data-as-artifacts) so that we can download
them for debugging.
Usually, you only need to run the tests that you are working on, for example:
```bash
go test -run=^TestClick$
```
The above will only run TestClick.
### Disable headless mode
```bash
go test -rod=show
```
Check [defaults](../lib/defaults/defaults.go) for other available options.
### Lint project
You can run all commands inside Docker so that you don't have to install all the development dependencies.
Check [Use Docker for development](#use-docker-for-development) for more info.
```bash
go generate # only required for first time
go run ./lib/utils/lint
```
### Code Coverage
If the code coverage is less than 100%, the CI will fail.
Learn the [basics](https://blog.golang.org/cover) first.
To visually see the coverage report you can run something like this:
```bash
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
```
It will open a web page to tell you which line is not covered.
To cover the error branch of the code we usually intercept cdp calls.
There are several helper functions for it:
- `rod_test.MockClient.stubCounter`
- `rod_test.MockClient.stub`
- `rod_test.MockClient.stubErr`
### Use Docker for development
1. Build the test image: `docker build -t rod -f lib/docker/dev.Dockerfile .`
1. Run a container with and mount the cache volume to it: `docker run -v $(pwd):/t --name rod -it rod bash`
1. Open another terminal, copy your global git-ignore file to the container: `docker cp ~/.gitignore_global rod:/root/`
1. Run lint in the container: `go run ./lib/utils/lint`
1. Run tests in the container: `go test`
1. After you exit the container with `exit`, you can restart it by: `docker start -i rod`
### Deployment of docker images
We use `.github/workflows/docker.yml` to automate it.
### Detect goroutine leak
Because parallel execution will pollution the global goroutine stack, by default, the goroutine leak detection for each test will be disabled, but the detection for the whole test program will still work as well. To enable detection for each test, just use `go test -parallel=1`.
### Debug dependency libs
Run `go mod vendor` to create a local mirror of dependencies.
The Golang compiler will use the libs under `vendor` folder as a priority.
For example, we can modify file `./vendor/github.com/ysmood/goob/goob.go` to debug, such as add some extra logs.
## Comments
All conversations in Github issues, PRs, etc. should be summarized into code comments so that this project is not deep coupled with Github service.
## Convention of the git commit message
The commit message follows the rules [here](https://github.com/torvalds/subsurface-for-dirk/blame/a48494d2fbed58c751e9b7e8fbff88582f9b2d02/README#L88). We don't use rules like [Conventional Commits](https://www.conventionalcommits.org/) because it's hard for beginners to write correct commit messages. It will encourage reviewers to spend more time on high-level problems, not the details. We also want to reduce the overhead when reading the git-blame, for example, `fix: correct minor typos in code` is the same as `fix minor typos in code`, there's no need to repeat content in the title line.
golang-github-go-rod-rod-0.116.2/.github/FUNDING.yml 0000664 0000000 0000000 00000000021 15206741465 0021537 0 ustar 00root root 0000000 0000000 github: [ysmood]
golang-github-go-rod-rod-0.116.2/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15206741465 0022114 5 ustar 00root root 0000000 0000000 golang-github-go-rod-rod-0.116.2/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000000156 15206741465 0025643 0 ustar 00root root 0000000 0000000 ---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhance
assignees: ''
---
golang-github-go-rod-rod-0.116.2/.github/ISSUE_TEMPLATE/question.md 0000664 0000000 0000000 00000001640 15206741465 0024306 0 ustar 00root root 0000000 0000000 ---
name: Question
about: Title of your question.
title: ''
labels: question
assignees: ''
---
Rod Version: v0.0.0
## The code to demonstrate your question
1. Clone Rod to your local and cd to the repository:
```bash
git clone https://github.com/go-rod/rod
cd rod
```
1. Use your code to replace the content of function `TestRod` in file `rod_test.go`.
1. Test your code with: `go test -run TestRod`, make sure it fails as expected.
1. Replace ALL THE CONTENT under "The code to demonstrate your question" with your `TestRod` function, like below:
```go
func TestRod(t *testing.T) {
g := setup(t)
g.Eq(1, 2) // the test should fail, here 1 doesn't equal 2
}
```
## What you got
Such as what error you see.
## What you expect to see
Such as what you want to do.
## What have you tried to solve the question
Such as after modifying some source code of Rod you are able to get rid of the problem.
golang-github-go-rod-rod-0.116.2/.github/pull_request_template.md 0000664 0000000 0000000 00000000261 15206741465 0024671 0 ustar 00root root 0000000 0000000 # Development guide
[Link](https://github.com/go-rod/rod/blob/main/.github/CONTRIBUTING.md)
## Test on local before making the PR
```bash
go run ./lib/utils/simple-check
```
golang-github-go-rod-rod-0.116.2/.github/workflows/ 0000775 0000000 0000000 00000000000 15206741465 0021766 5 ustar 00root root 0000000 0000000 golang-github-go-rod-rod-0.116.2/.github/workflows/check-examples.yml 0000664 0000000 0000000 00000000555 15206741465 0025407 0 ustar 00root root 0000000 0000000 name: Check Examples
on:
schedule:
- cron: '23 3 * * *'
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: actions/checkout@v4
- run: |
go run ./lib/utils/get-browser
go test -run Example ./...
go test ./lib/examples/e2e-testing
golang-github-go-rod-rod-0.116.2/.github/workflows/check-issues.yml 0000664 0000000 0000000 00000000607 15206741465 0025102 0 ustar 00root root 0000000 0000000 name: Check Issues
on:
issues:
types: [opened, edited]
permissions:
issues: write
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: actions/checkout@v4
- name: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: cd lib/utils/check-issue && go run .
golang-github-go-rod-rod-0.116.2/.github/workflows/check-revision.yml 0000664 0000000 0000000 00000000472 15206741465 0025425 0 ustar 00root root 0000000 0000000 name: Check Revision
on:
schedule:
- cron: '0 0 1 * *' # monthly
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: actions/checkout@v4
- run: |
go run ./lib/launcher/revision
go generate
golang-github-go-rod-rod-0.116.2/.github/workflows/docker.yml 0000664 0000000 0000000 00000002671 15206741465 0023766 0 ustar 00root root 0000000 0000000 # When git main branch changes it will build a image based on the main branch, the tag of the image will be latest.
# When a git semver tag is pushed it will build a image based on it, the tag will be the same as git's.
# It will do nothing on other git events.
# For the usage of the image, check lib/examples/launch-managed .
name: Release docker image
on: [push, pull_request]
permissions:
packages: write
jobs:
# TODO: we should merge job docker-amd and job docker-arm once the github actions fix their issue with cross-platform building.
docker-amd:
runs-on: ubuntu-22.04
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: docker/setup-qemu-action@v3
- uses: actions/checkout@v4
- run: go run ./lib/docker $GITHUB_REF
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- uses: actions/upload-artifact@v4
with:
name: review-fonts-docker
path: tmp/fonts.pdf
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: cdp-log-docker
path: tmp/cdp-log
docker-arm:
runs-on: ubuntu-22.04
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: docker/setup-qemu-action@v3
- uses: actions/checkout@v4
- run: go run ./lib/docker $GITHUB_REF
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
ARCH: arm
golang-github-go-rod-rod-0.116.2/.github/workflows/test-linux.yml 0000664 0000000 0000000 00000001332 15206741465 0024624 0 ustar 00root root 0000000 0000000 name: Test Linux
on:
push:
branches:
- '**'
pull_request:
schedule:
- cron: '17 5 * * *'
jobs:
test-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: actions/checkout@v4
- run: go generate
- run: go run ./lib/utils/ci-test -race -coverprofile=coverage.out -run=^Test . ./lib/utils ./lib/proto ./lib/cdp ./lib/defaults ./lib/devices ./lib/launcher ./lib/input
- run: go run github.com/ysmood/got/cmd/check-cov@latest
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: cdp-log-linux
path: |
tmp/cdp-log
coverage.out
golang-github-go-rod-rod-0.116.2/.github/workflows/test-other-platforms.yml 0000664 0000000 0000000 00000002137 15206741465 0026617 0 ustar 00root root 0000000 0000000 name: Test Other Platforms
on:
push:
branches:
- '**'
pull_request:
jobs:
test-mac:
runs-on: macos-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: actions/checkout@v4
- run: go run ./lib/utils/ci-test -timeout-each=2m -run=^Test
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: cdp-log-macos
path: tmp/cdp-log
test-windows:
runs-on: windows-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: actions/checkout@v4
- run: go run ./lib/utils/ci-test -timeout-each=2m -run=^Test
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: cdp-log-windows
path: tmp/cdp-log
test-old-go:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.18
- uses: actions/checkout@v4
# As long as the build works we don't have to run tests.
- run: go build ./lib/examples/translator
golang-github-go-rod-rod-0.116.2/.gitignore 0000664 0000000 0000000 00000000103 15206741465 0020353 0 ustar 00root root 0000000 0000000 vendor/
node_modules/
tmp/
.git
.dockerignore
*.out
*.test
*.json
golang-github-go-rod-rod-0.116.2/.golangci.yml 0000664 0000000 0000000 00000003364 15206741465 0020763 0 ustar 00root root 0000000 0000000 linters:
enable-all: true
disable:
- gochecknoinits
- paralleltest
- wrapcheck
- gosec
- gochecknoglobals
- musttag
- varnamelen
- wsl
- nonamedreturns
- tagliatelle
- nlreturn
- nakedret
- gomnd
- mnd
- err113
- exhaustruct
- godox
- depguard
- testpackage
- exhaustive
- containedctx
- prealloc
- perfsprint
- ireturn
- contextcheck
- canonicalheader
- copyloopvar
- intrange
# Deprecated ones:
- execinquery
- structcheck
- interfacer
- deadcode
- varcheck
- ifshort
- exhaustivestruct
- golint
- maligned
- nosnakecase
- scopelint
linters-settings:
cyclop:
max-complexity: 15
gocyclo:
min-complexity: 15
nestif:
min-complexity: 6
funlen:
lines: 120
issues:
exclude-use-default: false
exclude-rules:
- path: _test.go$
linters:
- lll
- funlen
- dupword
- goconst
- contextcheck
- errorlint
- testableexamples
- forcetypeassert
# Generated code
- path: lib/proto/
linters:
- lll
- gocritic
- dupword
- forcetypeassert
- path: lib/devices/list.go
linters:
- lll
- path: lib/js/helper.go
linters:
- lll
- path: /fixtures/
linters:
- forbidigo
- path: lib/examples/
linters:
- forbidigo
- noctx
- gocritic
- path: examples?_test.go$
linters:
- forbidigo
- noctx
- gocritic
- path: main.go$
linters:
- forbidigo
- noctx
- forcetypeassert
- lll
- path: lib/assets/
linters:
- lll
golang-github-go-rod-rod-0.116.2/.prettierrc.yml 0000664 0000000 0000000 00000000062 15206741465 0021353 0 ustar 00root root 0000000 0000000 semi: false
singleQuote: true
trailingComma: none
golang-github-go-rod-rod-0.116.2/LICENSE 0000664 0000000 0000000 00000002051 15206741465 0017374 0 ustar 00root root 0000000 0000000 The MIT License
Copyright 2019 Yad Smood
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. golang-github-go-rod-rod-0.116.2/README.md 0000664 0000000 0000000 00000005611 15206741465 0017653 0 ustar 00root root 0000000 0000000 # Overview
[](https://pkg.go.dev/github.com/go-rod/rod)
[][discord room]
## [Documentation](https://go-rod.github.io/) | [API reference](https://pkg.go.dev/github.com/go-rod/rod?tab=doc) | [FAQ](https://go-rod.github.io/#/faq/README)
Rod is a high-level driver directly based on [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol).
It's designed for web automation and scraping for both high-level and low-level use, senior developers can use the low-level packages and functions to easily
customize or build up their own version of Rod, the high-level functions are just examples to build a default version of Rod.
[中文 API 文档](https://pkg.go.dev/github.com/go-rod/go-rod-chinese)
## Features
- Chained context design, intuitive to timeout or cancel the long-running task
- Auto-wait elements to be ready
- Debugging friendly, auto input tracing, remote monitoring headless browser
- Thread-safe for all operations
- Automatically find or download [browser](lib/launcher)
- High-level helpers like WaitStable, WaitRequestIdle, HijackRequests, WaitDownload, etc
- Two-step WaitEvent design, never miss an event ([how it works](https://github.com/ysmood/goob))
- Correctly handles nested iframes or shadow DOMs
- No zombie browser process after the crash ([how it works](https://github.com/ysmood/leakless))
- [CI](https://github.com/go-rod/rod/actions) enforced 100% test coverage
## Examples
Please check the [examples_test.go](examples_test.go) file first, then check the [examples](lib/examples) folder.
For more detailed examples, please search the unit tests.
Such as the usage of method `HandleAuth`, you can search all the `*_test.go` files that contain `HandleAuth`,
for example, use Github online [search in repository](https://github.com/go-rod/rod/search?q=HandleAuth&unscoped_q=HandleAuth).
You can also search the GitHub [issues](https://github.com/go-rod/rod/issues) or [discussions](https://github.com/go-rod/rod/discussions),
a lot of usage examples are recorded there.
[Here](lib/examples/compare-chromedp) is a comparison of the examples between rod and Chromedp.
If you have questions, please raise an [issues](https://github.com/go-rod/rod/issues)/[discussions](https://github.com/go-rod/rod/discussions) or join the [chat room][discord room].
## Join us
Your help is more than welcome! Even just open an issue to ask a question may greatly help others.
Please read [How To Ask Questions The Smart Way](http://www.catb.org/~esr/faqs/smart-questions.html) before you ask questions.
We use Github Projects to manage tasks, you can see the priority and progress of the issues [here](https://github.com/go-rod/rod/projects).
If you want to contribute please read the [Contributor Guide](.github/CONTRIBUTING.md).
[discord room]: https://discord.gg/CpevuvY
golang-github-go-rod-rod-0.116.2/browser.go 0000664 0000000 0000000 00000033772 15206741465 0020417 0 ustar 00root root 0000000 0000000 //go:generate go run ./lib/utils/setup
//go:generate go run ./lib/proto/generate
//go:generate go run ./lib/js/generate
//go:generate go run ./lib/assets/generate
//go:generate go run ./lib/utils/lint
// Package rod is a high-level driver directly based on DevTools Protocol.
package rod
import (
"context"
"reflect"
"strings"
"sync"
"time"
"github.com/go-rod/rod/lib/cdp"
"github.com/go-rod/rod/lib/defaults"
"github.com/go-rod/rod/lib/devices"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/rod/lib/utils"
"github.com/ysmood/goob"
)
// Browser implements these interfaces.
var (
_ proto.Client = &Browser{}
_ proto.Contextable = &Browser{}
)
// Browser represents the browser.
// It doesn't depends on file system, it should work with remote browser seamlessly.
// To check the env var you can use to quickly enable options from CLI, check here:
// https://pkg.go.dev/github.com/go-rod/rod/lib/defaults
type Browser struct {
// BrowserContextID is the id for incognito window
BrowserContextID proto.BrowserBrowserContextID
e eFunc
ctx context.Context
sleeper func() utils.Sleeper
logger utils.Logger
slowMotion time.Duration // see defaults.slow
trace bool // see defaults.Trace
monitor string
defaultDevice devices.Device
controlURL string
client CDPClient
event *goob.Observable // all the browser events from cdp client
targetsLock *sync.Mutex
// stores all the previous cdp call of same type. Browser doesn't have enough API
// for us to retrieve all its internal states. This is an workaround to map them to local.
// For example you can't use cdp API to get the current position of mouse.
states *sync.Map
}
// New creates a controller.
// DefaultDevice to emulate is set to [devices.LaptopWithMDPIScreen].Landscape(), it will change the default
// user-agent and can make the actual view area smaller than the browser window on headful mode,
// you can use [Browser.NoDefaultDevice] to disable it.
func New() *Browser {
return (&Browser{
ctx: context.Background(),
sleeper: DefaultSleeper,
controlURL: defaults.URL,
slowMotion: defaults.Slow,
trace: defaults.Trace,
monitor: defaults.Monitor,
logger: DefaultLogger,
defaultDevice: devices.LaptopWithMDPIScreen.Landscape(),
targetsLock: &sync.Mutex{},
states: &sync.Map{},
}).WithPanic(utils.Panic)
}
// Incognito creates a new incognito browser.
func (b *Browser) Incognito() (*Browser, error) {
res, err := proto.TargetCreateBrowserContext{}.Call(b)
if err != nil {
return nil, err
}
incognito := *b
incognito.BrowserContextID = res.BrowserContextID
return &incognito, nil
}
// ControlURL set the url to remote control browser.
func (b *Browser) ControlURL(url string) *Browser {
b.controlURL = url
return b
}
// SlowMotion set the delay for each control action, such as the simulation of the human inputs.
func (b *Browser) SlowMotion(delay time.Duration) *Browser {
b.slowMotion = delay
return b
}
// Trace enables/disables the visual tracing of the input actions on the page.
func (b *Browser) Trace(enable bool) *Browser {
b.trace = enable
return b
}
// Monitor address to listen if not empty. Shortcut for [Browser.ServeMonitor].
func (b *Browser) Monitor(url string) *Browser {
b.monitor = url
return b
}
// Logger overrides the default log functions for tracing.
func (b *Browser) Logger(l utils.Logger) *Browser {
b.logger = l
return b
}
// Client set the cdp client.
func (b *Browser) Client(c CDPClient) *Browser {
b.client = c
return b
}
// DefaultDevice sets the default device for new page to emulate in the future.
// Default is [devices.LaptopWithMDPIScreen].
// Set it to [devices.Clear] to disable it.
func (b *Browser) DefaultDevice(d devices.Device) *Browser {
b.defaultDevice = d
return b
}
// NoDefaultDevice is the same as [Browser.DefaultDevice](devices.Clear).
func (b *Browser) NoDefaultDevice() *Browser {
return b.DefaultDevice(devices.Clear)
}
// Connect to the browser and start to control it.
// If fails to connect, try to launch a local browser, if local browser not found try to download one.
func (b *Browser) Connect() error {
if b.client == nil {
u := b.controlURL
if u == "" {
var err error
u, err = launcher.New().Context(b.ctx).Launch()
if err != nil {
return err
}
}
c, err := cdp.StartWithURL(b.ctx, u, nil)
if err != nil {
return err
}
b.client = c
} else if b.controlURL != "" {
panic("Browser.Client and Browser.ControlURL can't be set at the same time")
}
b.initEvents()
if b.monitor != "" {
launcher.Open(b.ServeMonitor(b.monitor))
}
return proto.TargetSetDiscoverTargets{Discover: true}.Call(b)
}
// Close the browser.
func (b *Browser) Close() error {
if b.BrowserContextID == "" {
return proto.BrowserClose{}.Call(b)
}
return proto.TargetDisposeBrowserContext{BrowserContextID: b.BrowserContextID}.Call(b)
}
// Page creates a new browser tab. If opts.URL is empty, the default target will be "about:blank".
func (b *Browser) Page(opts proto.TargetCreateTarget) (p *Page, err error) {
req := opts
req.BrowserContextID = b.BrowserContextID
req.URL = "about:blank"
target, err := req.Call(b)
if err != nil {
return nil, err
}
defer func() {
// If Navigate or PageFromTarget fails we should close the target to prevent leak
if err != nil {
_, _ = proto.TargetCloseTarget{TargetID: target.TargetID}.Call(b)
}
}()
p, err = b.PageFromTarget(target.TargetID)
if err != nil {
return
}
if opts.URL == "" {
return
}
err = p.Navigate(opts.URL)
return
}
// Pages retrieves all visible pages.
func (b *Browser) Pages() (Pages, error) {
list, err := proto.TargetGetTargets{}.Call(b)
if err != nil {
return nil, err
}
pageList := Pages{}
for _, target := range list.TargetInfos {
if target.Type != proto.TargetTargetInfoTypePage {
continue
}
page, err := b.PageFromTarget(target.TargetID)
if err != nil {
return nil, err
}
pageList = append(pageList, page)
}
return pageList, nil
}
// Call implements the [proto.Client] to call raw cdp interface directly.
func (b *Browser) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) {
res, err = b.client.Call(ctx, sessionID, methodName, params)
if err != nil {
return nil, err
}
b.set(proto.TargetSessionID(sessionID), methodName, params)
return
}
// PageFromSession is used for low-level debugging.
func (b *Browser) PageFromSession(sessionID proto.TargetSessionID) *Page {
sessionCtx, cancel := context.WithCancel(b.ctx)
return &Page{
e: b.e,
ctx: sessionCtx,
sessionCancel: cancel,
sleeper: b.sleeper,
browser: b,
SessionID: sessionID,
}
}
// PageFromTarget gets or creates a Page instance.
func (b *Browser) PageFromTarget(targetID proto.TargetTargetID) (*Page, error) {
b.targetsLock.Lock()
defer b.targetsLock.Unlock()
page := b.loadCachedPage(targetID)
if page != nil {
return page, nil
}
session, err := proto.TargetAttachToTarget{
TargetID: targetID,
Flatten: true, // if it's not set no response will return
}.Call(b)
if err != nil {
return nil, err
}
sessionCtx, cancel := context.WithCancel(b.ctx)
page = &Page{
e: b.e,
ctx: sessionCtx,
sessionCancel: cancel,
sleeper: b.sleeper,
browser: b,
TargetID: targetID,
SessionID: session.SessionID,
FrameID: proto.PageFrameID(targetID),
jsCtxLock: &sync.Mutex{},
jsCtxID: new(proto.RuntimeRemoteObjectID),
helpersLock: &sync.Mutex{},
}
page.root = page
page.newKeyboard().newMouse().newTouch()
if !b.defaultDevice.IsClear() {
err = page.Emulate(b.defaultDevice)
if err != nil {
return nil, err
}
}
b.cachePage(page)
page.initEvents()
// If we don't enable it, it will cause a lot of unexpected browser behavior.
// Such as proto.PageAddScriptToEvaluateOnNewDocument won't work.
page.EnableDomain(&proto.PageEnable{})
return page, nil
}
// EachEvent is similar to [Page.EachEvent], but catches events of the entire browser.
func (b *Browser) EachEvent(callbacks ...interface{}) (wait func()) {
return b.eachEvent("", callbacks...)
}
// WaitEvent waits for the next event for one time. It will also load the data into the event object.
func (b *Browser) WaitEvent(e proto.Event) (wait func()) {
return b.waitEvent("", e)
}
// waits for the next event for one time. It will also load the data into the event object.
func (b *Browser) waitEvent(sessionID proto.TargetSessionID, e proto.Event) (wait func()) {
valE := reflect.ValueOf(e)
valTrue := reflect.ValueOf(true)
if valE.Kind() != reflect.Ptr {
valE = reflect.New(valE.Type())
}
// dynamically creates a function on runtime:
//
// func(ee proto.Event) bool {
// *e = *ee
// return true
// }
fnType := reflect.FuncOf([]reflect.Type{valE.Type()}, []reflect.Type{valTrue.Type()}, false)
fnVal := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {
valE.Elem().Set(args[0].Elem())
return []reflect.Value{valTrue}
})
return b.eachEvent(sessionID, fnVal.Interface())
}
// If the any callback returns true the event loop will stop.
// It will enable the related domains if not enabled, and restore them after wait ends.
func (b *Browser) eachEvent(sessionID proto.TargetSessionID, callbacks ...interface{}) (wait func()) {
cbMap := map[string]reflect.Value{}
restores := []func(){}
for _, cb := range callbacks {
cbVal := reflect.ValueOf(cb)
eType := cbVal.Type().In(0)
name := reflect.New(eType.Elem()).Interface().(proto.Event).ProtoEvent() //nolint: forcetypeassert
cbMap[name] = cbVal
// Only enabled domains will emit events to cdp client.
// We enable the domains for the event types if it's not enabled.
// We restore the domains to their previous states after the wait ends.
domain, _ := proto.ParseMethodName(name)
if req := proto.GetType(domain + ".enable"); req != nil {
enable := reflect.New(req).Interface().(proto.Request) //nolint: forcetypeassert
restores = append(restores, b.EnableDomain(sessionID, enable))
}
}
b, cancel := b.WithCancel()
messages := b.Event()
return func() {
if messages == nil {
panic("can't use wait function twice")
}
defer func() {
cancel()
messages = nil
for _, restore := range restores {
restore()
}
}()
for msg := range messages {
if !(sessionID == "" || msg.SessionID == sessionID) {
continue
}
if cbVal, has := cbMap[msg.Method]; has {
e := reflect.New(proto.GetType(msg.Method))
msg.Load(e.Interface().(proto.Event)) //nolint: forcetypeassert
args := []reflect.Value{e}
if cbVal.Type().NumIn() == 2 {
args = append(args, reflect.ValueOf(msg.SessionID))
}
res := cbVal.Call(args)
if len(res) > 0 {
if res[0].Bool() {
return
}
}
}
}
}
}
// Event of the browser.
func (b *Browser) Event() <-chan *Message {
src := b.event.Subscribe(b.ctx)
dst := make(chan *Message)
go func() {
defer close(dst)
for {
select {
case <-b.ctx.Done():
return
case e, ok := <-src:
if !ok {
return
}
select {
case <-b.ctx.Done():
return
case dst <- e.(*Message): //nolint: forcetypeassert
}
}
}
}()
return dst
}
func (b *Browser) initEvents() {
ctx, cancel := context.WithCancel(b.ctx)
b.event = goob.New(ctx)
event := b.client.Event()
go func() {
defer cancel()
for e := range event {
b.event.Publish(&Message{
SessionID: proto.TargetSessionID(e.SessionID),
Method: e.Method,
lock: &sync.Mutex{},
data: e.Params,
})
}
}()
}
func (b *Browser) pageInfo(id proto.TargetTargetID) (*proto.TargetTargetInfo, error) {
res, err := proto.TargetGetTargetInfo{TargetID: id}.Call(b)
if err != nil {
return nil, err
}
return res.TargetInfo, nil
}
func (b *Browser) isHeadless() (enabled bool) {
res, _ := proto.BrowserGetBrowserCommandLine{}.Call(b)
for _, v := range res.Arguments {
if strings.Contains(v, "headless") {
return true
}
}
return false
}
// IgnoreCertErrors switch. If enabled, all certificate errors will be ignored.
func (b *Browser) IgnoreCertErrors(enable bool) error {
return proto.SecuritySetIgnoreCertificateErrors{Ignore: enable}.Call(b)
}
// GetCookies from the browser.
func (b *Browser) GetCookies() ([]*proto.NetworkCookie, error) {
res, err := proto.StorageGetCookies{BrowserContextID: b.BrowserContextID}.Call(b)
if err != nil {
return nil, err
}
return res.Cookies, nil
}
// SetCookies to the browser. If the cookies is nil it will clear all the cookies.
func (b *Browser) SetCookies(cookies []*proto.NetworkCookieParam) error {
if cookies == nil {
return proto.StorageClearCookies{BrowserContextID: b.BrowserContextID}.Call(b)
}
return proto.StorageSetCookies{
Cookies: cookies,
BrowserContextID: b.BrowserContextID,
}.Call(b)
}
// WaitDownload returns a helper to get the next download file.
// The file path will be:
//
// filepath.Join(dir, info.GUID)
func (b *Browser) WaitDownload(dir string) func() (info *proto.PageDownloadWillBegin) {
var oldDownloadBehavior proto.BrowserSetDownloadBehavior
has := b.LoadState("", &oldDownloadBehavior)
_ = proto.BrowserSetDownloadBehavior{
Behavior: proto.BrowserSetDownloadBehaviorBehaviorAllowAndName,
BrowserContextID: b.BrowserContextID,
DownloadPath: dir,
}.Call(b)
var start *proto.PageDownloadWillBegin
waitProgress := b.EachEvent(func(e *proto.PageDownloadWillBegin) {
start = e
}, func(e *proto.PageDownloadProgress) bool {
return start != nil && start.GUID == e.GUID && e.State == proto.PageDownloadProgressStateCompleted
})
return func() *proto.PageDownloadWillBegin {
defer func() {
if has {
_ = oldDownloadBehavior.Call(b)
} else {
_ = proto.BrowserSetDownloadBehavior{
Behavior: proto.BrowserSetDownloadBehaviorBehaviorDefault,
BrowserContextID: b.BrowserContextID,
}.Call(b)
}
}()
waitProgress()
return start
}
}
// Version info of the browser.
func (b *Browser) Version() (*proto.BrowserGetVersionResult, error) {
return proto.BrowserGetVersion{}.Call(b)
}
golang-github-go-rod-rod-0.116.2/browser_test.go 0000664 0000000 0000000 00000023422 15206741465 0021445 0 ustar 00root root 0000000 0000000 package rod_test
import (
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"runtime"
"testing"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/cdp"
"github.com/go-rod/rod/lib/devices"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/rod/lib/utils"
"github.com/ysmood/got"
"github.com/ysmood/gson"
)
func TestIncognito(t *testing.T) {
g := setup(t)
k := g.RandStr(16)
b := g.browser.MustIncognito().Sleeper(rod.DefaultSleeper)
defer b.MustClose()
page := b.MustPage(g.blank())
defer page.MustClose()
page.MustEval(`k => localStorage[k] = 1`, k)
g.True(g.page.MustNavigate(g.blank()).MustEval(`k => localStorage[k]`, k).Nil())
g.Eq(page.MustEval(`k => localStorage[k]`, k).Str(), "1") // localStorage can only store string
g.Panic(func() {
g.mc.stubErr(1, proto.TargetCreateBrowserContext{})
g.browser.MustIncognito()
})
}
func TestBrowserResetControlURL(_ *testing.T) {
rod.New().ControlURL("test").ControlURL("")
}
func TestDefaultDevice(t *testing.T) {
g := setup(t)
ua := ""
s := g.Serve()
s.Mux.HandleFunc("/t", func(_ http.ResponseWriter, r *http.Request) {
ua = r.Header.Get("User-Agent")
})
// TODO: https://github.com/golang/go/issues/51459
b := *g.browser
b.DefaultDevice(devices.IPhoneX)
b.MustPage(s.URL("/t")).MustClose()
g.Eq(ua, devices.IPhoneX.UserAgentEmulation().UserAgent)
b.NoDefaultDevice()
b.MustPage(s.URL("/t")).MustClose()
g.Neq(ua, devices.IPhoneX.UserAgentEmulation().UserAgent)
}
func TestPageErr(t *testing.T) {
g := setup(t)
g.Panic(func() {
g.mc.stubErr(1, proto.TargetAttachToTarget{})
g.browser.MustPage()
})
}
func TestPageFromTarget(t *testing.T) {
g := setup(t)
g.Panic(func() {
res, err := proto.TargetCreateTarget{URL: "about:blank"}.Call(g.browser)
g.E(err)
defer func() {
g.browser.MustPageFromTargetID(res.TargetID).MustClose()
}()
g.mc.stubErr(1, proto.EmulationSetDeviceMetricsOverride{})
g.browser.MustPageFromTargetID(res.TargetID)
})
}
func TestBrowserPages(t *testing.T) {
g := setup(t)
b := g.browser
pages := b.MustPages()
g.Gte(len(pages), 1)
{
g.mc.stub(1, proto.TargetGetTargets{}, func(send StubSend) (gson.JSON, error) {
d, _ := send()
return *d.Set("targetInfos.0.type", "iframe"), nil
})
b.MustPages()
}
g.Panic(func() {
g.mc.stubErr(1, proto.TargetCreateTarget{})
b.MustPage()
})
g.Panic(func() {
g.mc.stubErr(1, proto.TargetGetTargets{})
b.MustPages()
})
g.Panic(func() {
_, err := proto.TargetCreateTarget{URL: "about:blank"}.Call(b)
g.E(err)
g.mc.stubErr(1, proto.TargetAttachToTarget{})
b.MustPages()
})
}
func TestBrowserClearStates(t *testing.T) {
g := setup(t)
g.E(proto.EmulationClearGeolocationOverride{}.Call(g.page))
}
func TestBrowserEvent(t *testing.T) {
g := setup(t)
messages := g.browser.Context(g.Context()).Event()
p := g.newPage()
wait := make(chan struct{})
for msg := range messages {
e := proto.TargetAttachedToTarget{}
if msg.Load(&e) {
g.Eq(e.TargetInfo.TargetID, p.TargetID)
close(wait)
break
}
}
<-wait
}
func TestBrowserWaitEvent(t *testing.T) {
g := setup(t)
g.NotNil(g.browser.Context(g.Context()).Event())
wait := g.page.WaitEvent(proto.PageFrameNavigated{})
g.page.MustNavigate(g.blank())
wait()
wait = g.browser.EachEvent(func(_ *proto.PageFrameNavigated, _ proto.TargetSessionID) bool {
return true
})
g.page.MustNavigate(g.blank())
wait()
}
func TestBrowserCrash(t *testing.T) {
g := setup(t)
browser := rod.New().Context(g.Context()).MustConnect()
page := browser.MustPage()
js := `() => new Promise(r => setTimeout(r, 10000))`
go g.Panic(func() {
page.MustEval(js)
})
utils.Sleep(0.2)
_ = proto.BrowserCrash{}.Call(browser)
utils.Sleep(0.3)
_, err := page.Eval(js)
g.Has(err.Error(), "use of closed network connection")
}
func TestBrowserCall(t *testing.T) {
g := setup(t)
v, err := proto.BrowserGetVersion{}.Call(g.browser)
g.E(err)
g.Regex("1.3", v.ProtocolVersion)
}
func TestBlockingNavigation(t *testing.T) {
g := setup(t)
/*
Navigate can take forever if a page doesn't response.
If one page is blocked, other pages should still work.
*/
s := g.Serve()
pause := g.Context()
s.Mux.HandleFunc("/a", func(_ http.ResponseWriter, _ *http.Request) {
<-pause.Done()
})
s.Route("/b", ".html", `ok`)
blocked := g.newPage()
go func() {
g.Panic(func() {
blocked.MustNavigate(s.URL("/a"))
})
}()
utils.Sleep(0.3)
g.newPage(s.URL("/b"))
}
func TestResolveBlocking(t *testing.T) {
g := setup(t)
s := g.Serve()
pause := g.Context()
s.Mux.HandleFunc("/", func(_ http.ResponseWriter, _ *http.Request) {
<-pause.Done()
})
p := g.newPage()
go func() {
utils.Sleep(0.1)
p.MustStopLoading()
}()
g.Panic(func() {
p.MustNavigate(s.URL())
})
}
func TestTestTry(t *testing.T) {
g := setup(t)
g.Nil(rod.Try(func() {}))
err := rod.Try(func() { panic(1) })
var errVal *rod.TryError
g.True(errors.As(err, &errVal))
g.Is(err, &rod.TryError{})
g.Eq(errVal.Unwrap().Error(), "1")
g.Eq(1, errVal.Value)
g.Has(errVal.Error(), "error value: 1\ngoroutine")
errVal = rod.Try(func() { panic(errors.New("t")) }).(*rod.TryError)
g.Eq(errVal.Unwrap().Error(), "t")
}
func TestBrowserOthers(t *testing.T) {
g := setup(t)
g.browser.Timeout(time.Second).CancelTimeout().MustGetCookies()
}
func TestBinarySize(t *testing.T) {
g := setup(t)
if runtime.GOOS == "windows" || utils.InContainer {
g.SkipNow()
}
cmd := exec.Command("go", "build",
"-trimpath",
"-ldflags", "-w -s",
"-o", "tmp/translator",
"./lib/examples/translator")
cmd.Env = append(os.Environ(), "GOOS=linux")
g.Nil(cmd.Run())
stat, err := os.Stat("tmp/translator")
g.E(err)
g.Lte(float64(stat.Size())/1024/1024, 11) // mb
}
func TestBrowserCookies(t *testing.T) {
g := setup(t)
b := g.browser.MustIncognito()
defer b.MustClose()
b.MustSetCookies(&proto.NetworkCookie{
Name: "a",
Value: "val",
Domain: "test.com",
})
cookies := b.MustGetCookies()
g.Len(cookies, 1)
g.Eq(cookies[0].Name, "a")
g.Eq(cookies[0].Value, "val")
{
b.MustSetCookies()
cookies := b.MustGetCookies()
g.Len(cookies, 0)
}
g.mc.stubErr(1, proto.StorageGetCookies{})
g.Err(b.GetCookies())
}
func TestWaitDownload(t *testing.T) {
g := setup(t)
s := g.Serve()
content := "test content"
s.Route("/d", ".bin", []byte(content))
s.Route("/page", ".html", fmt.Sprintf(`click`, s.URL()))
page := g.page.MustNavigate(s.URL("/page"))
wait := g.browser.MustWaitDownload()
page.MustElement("a").MustClick()
data := wait()
g.Eq(content, string(data))
}
func TestWaitDownloadDataURI(t *testing.T) {
g := setup(t)
s := g.Serve()
s.Route("/", ".html",
`
click
click
`,
)
page := g.page.MustNavigate(s.URL())
wait1 := g.browser.MustWaitDownload()
page.MustElement("#a").MustClick()
data := wait1()
g.Eq("test data", string(data))
wait2 := g.browser.MustWaitDownload()
page.MustElement("#b").MustClick()
data = wait2()
g.Eq("test blob", string(data))
}
func TestWaitDownloadCancel(t *testing.T) {
g := setup(t)
wait := g.browser.Context(g.Timeout(0)).WaitDownload(os.TempDir())
g.Eq(wait(), (*proto.PageDownloadWillBegin)(nil))
}
func TestWaitDownloadFromNewPage(t *testing.T) {
g := setup(t)
s := g.Serve()
content := "test content"
s.Route("/d", ".bin", content)
s.Route("/page", ".html", fmt.Sprintf(
`click`,
s.URL()),
)
page := g.page.MustNavigate(s.URL("/page"))
wait := g.browser.MustWaitDownload()
page.MustElement("a").MustClick()
data := wait()
g.Eq(content, string(data))
}
func TestBrowserConnectErr(t *testing.T) {
g := setup(t)
g.Panic(func() {
rod.New().ControlURL(g.RandStr(16)).MustConnect()
})
}
func TestStreamReader(t *testing.T) {
g := setup(t)
r := rod.NewStreamReader(g.page, "")
g.mc.stub(1, proto.IORead{}, func(_ StubSend) (gson.JSON, error) {
return gson.New(proto.IOReadResult{
Data: "test",
}), nil
})
b := make([]byte, 4)
_, _ = r.Read(b)
g.Eq("test", string(b))
g.mc.stubErr(1, proto.IORead{})
_, err := r.Read(nil)
g.Err(err)
g.mc.stub(1, proto.IORead{}, func(_ StubSend) (gson.JSON, error) {
return gson.New(proto.IOReadResult{
Base64Encoded: true,
Data: "@",
}), nil
})
_, err = r.Read(nil)
g.Err(err)
}
func TestBrowserConnectFailure(t *testing.T) {
g := setup(t)
c := g.Context()
c.Cancel()
err := rod.New().Context(c).Connect()
if err == nil {
g.Fatal("expected an error on connect failure")
}
}
func TestBrowserPool(t *testing.T) {
g := got.T(t)
pool := rod.NewBrowserPool(3)
b, err := pool.Get(func() (*rod.Browser, error) {
browser := rod.New()
return browser, browser.Connect()
})
g.E(err)
pool.Put(b)
b = pool.MustGet(func() *rod.Browser { return rod.New().MustConnect() })
pool.Put(b)
pool.Cleanup(func(p *rod.Browser) {
p.MustClose()
})
}
func TestOldBrowser(t *testing.T) {
t.Skip()
g := setup(t)
u := launcher.New().Revision(686378).MustLaunch()
b := rod.New().ControlURL(u).MustConnect()
g.Cleanup(b.MustClose)
res, err := proto.BrowserGetVersion{}.Call(b)
g.E(err)
g.Eq(res.Revision, "@19d4547535ab5aba70b4730443f84e8153052174")
}
func TestBrowserLostConnection(t *testing.T) {
g := setup(t)
l := launcher.New()
p := rod.New().ControlURL(l.MustLaunch()).MustConnect().MustPage(g.blank())
go func() {
utils.Sleep(1)
l.Kill()
}()
_, err := p.Eval(`() => new Promise(r => {})`)
g.Err(err)
}
func TestBrowserConnectConflict(t *testing.T) {
g := setup(t)
g.Panic(func() {
rod.New().Client(&cdp.Client{}).ControlURL("test").MustConnect()
})
}
golang-github-go-rod-rod-0.116.2/context.go 0000664 0000000 0000000 00000007635 15206741465 0020417 0 ustar 00root root 0000000 0000000 package rod
import (
"context"
"time"
"github.com/go-rod/rod/lib/utils"
)
type (
timeoutContextKey struct{}
timeoutContextVal struct {
parent context.Context
cancel context.CancelFunc
}
)
// Context returns a clone with the specified ctx for chained sub-operations.
func (b *Browser) Context(ctx context.Context) *Browser {
newObj := *b
newObj.ctx = ctx
return &newObj
}
// GetContext of current instance.
func (b *Browser) GetContext() context.Context {
return b.ctx
}
// Timeout returns a clone with the specified total timeout of all chained sub-operations.
func (b *Browser) Timeout(d time.Duration) *Browser {
ctx, cancel := context.WithTimeout(b.ctx, d)
return b.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{b.ctx, cancel}))
}
// CancelTimeout cancels the current timeout context and returns a clone with the parent context.
func (b *Browser) CancelTimeout() *Browser {
val := b.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint:forcetypeassert
val.cancel()
return b.Context(val.parent)
}
// WithCancel returns a clone with a context cancel function.
func (b *Browser) WithCancel() (*Browser, func()) {
ctx, cancel := context.WithCancel(b.ctx)
return b.Context(ctx), cancel
}
// Sleeper returns a clone with the specified sleeper for chained sub-operations.
func (b *Browser) Sleeper(sleeper func() utils.Sleeper) *Browser {
newObj := *b
newObj.sleeper = sleeper
return &newObj
}
// Context returns a clone with the specified ctx for chained sub-operations.
func (p *Page) Context(ctx context.Context) *Page {
p.helpersLock.Lock()
newObj := *p
p.helpersLock.Unlock()
newObj.ctx = ctx
return &newObj
}
// GetContext of current instance.
func (p *Page) GetContext() context.Context {
return p.ctx
}
// Timeout returns a clone with the specified total timeout of all chained sub-operations.
func (p *Page) Timeout(d time.Duration) *Page {
ctx, cancel := context.WithTimeout(p.ctx, d)
return p.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{p.ctx, cancel}))
}
// CancelTimeout cancels the current timeout context and returns a clone with the parent context.
func (p *Page) CancelTimeout() *Page {
val := p.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint: forcetypeassert
val.cancel()
return p.Context(val.parent)
}
// WithCancel returns a clone with a context cancel function.
func (p *Page) WithCancel() (*Page, func()) {
ctx, cancel := context.WithCancel(p.ctx)
return p.Context(ctx), cancel
}
// Sleeper returns a clone with the specified sleeper for chained sub-operations.
func (p *Page) Sleeper(sleeper func() utils.Sleeper) *Page {
newObj := *p
newObj.sleeper = sleeper
return &newObj
}
// Context returns a clone with the specified ctx for chained sub-operations.
func (el *Element) Context(ctx context.Context) *Element {
newObj := *el
newObj.ctx = ctx
return &newObj
}
// GetContext of current instance.
func (el *Element) GetContext() context.Context {
return el.ctx
}
// Timeout returns a clone with the specified total timeout of all chained sub-operations.
func (el *Element) Timeout(d time.Duration) *Element {
ctx, cancel := context.WithTimeout(el.ctx, d)
return el.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{el.ctx, cancel}))
}
// CancelTimeout cancels the current timeout context and returns a clone with the parent context.
func (el *Element) CancelTimeout() *Element {
val := el.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint: forcetypeassert
val.cancel()
return el.Context(val.parent)
}
// WithCancel returns a clone with a context cancel function.
func (el *Element) WithCancel() (*Element, func()) {
ctx, cancel := context.WithCancel(el.ctx)
return el.Context(ctx), cancel
}
// Sleeper returns a clone with the specified sleeper for chained sub-operations.
func (el *Element) Sleeper(sleeper func() utils.Sleeper) *Element {
newObj := *el
newObj.sleeper = sleeper
return &newObj
}
golang-github-go-rod-rod-0.116.2/cspell.json 0000664 0000000 0000000 00000004703 15206741465 0020552 0 ustar 00root root 0000000 0000000 // cSpell Settings
{
// Version of the setting file. Always 0.2
"version": "0.2",
// language - current active spelling language
"language": "en",
"ignorePaths": [
"**/*.{out,sketch,svg}",
"fixtures/fonts.html",
"**/tmp/**",
"lib/devices/list.go",
"lib/js/helper.go",
"lib/proto/!(a_*)",
"**/go.{mod,sum}",
".golangci.yml"
],
// words - list of words to be always considered correct
"words": [
"APPDATA",
"Arraybuffer",
"backgrounding",
"backoff",
"Backquote",
"beforeunload",
"bodyclose",
"breakpad",
"Chromedp",
"codesearch",
"commandline",
"COMSPEC",
"containerenv",
"contenteditable",
"Contentful",
"Contextable",
"contextcheck",
"coverprofile",
"Dataview",
"datetime",
"dockerenv",
"dropzone",
"duckduckgo",
"enctype",
"errcheck",
"evenodd",
"excludesfile",
"fetchup",
"fontconfig",
"forbidigo",
"forcetypeassert",
"Fullscreen",
"Geolocation",
"getent",
"gobwas",
"gocognit",
"gocyclo",
"GODEBUG",
"gofmt",
"gofumpt",
"goimports",
"golangci",
"goob",
"gopls",
"goproxy",
"gotrace",
"gson",
"headful",
"iframe",
"iframes",
"Interactable",
"ioutil",
"keychain",
"KHTML",
"ldflags",
"leakless",
"libasound",
"libcairo",
"libgbm",
"libgobject",
"libgtk",
"libnss",
"libxss",
"libxtst",
"Lmsgprefix",
"loglevel",
"MDPI",
"MITM",
"mitmproxy",
"mvdan",
"nilnil",
"noctx",
"nolint",
"Noto",
"Numpad",
"onbeforeunload",
"onclick",
"onmouseenter",
"onmouseout",
"OOPIF",
"opencontainers",
"osversion",
"progresser",
"proto",
"proxyauth",
"Rects",
"repost",
"sattributes",
"schildren",
"Sessionable",
"Smood",
"Socketable",
"spki",
"spkis",
"srgb",
"staticcheck",
"stdlib",
"termux",
"tlid",
"touchend",
"touchstart",
"tparallel",
"tracebackancestors",
"trimpath",
"Typedarray",
"tzdata",
"Unserializable",
"Wasmvalue",
"Weakmap",
"Weakset",
"Webassemblymemory",
"wsutil",
"xlink",
"XVFB",
"ysmood"
],
// flagWords - list of words to be always considered incorrect
// This is useful for offensive words and common spelling errors.
// For example "hte" should be "the"
"flagWords": []
}
golang-github-go-rod-rod-0.116.2/dev_helpers.go 0000664 0000000 0000000 00000014263 15206741465 0021226 0 ustar 00root root 0000000 0000000 // This file defines the helpers to develop automation.
// Such as when running automation we can use trace to visually
// see where the mouse going to click.
package rod
import (
"encoding/json"
"fmt"
"html"
"net"
"net/http"
"strings"
"time"
"github.com/go-rod/rod/lib/assets"
"github.com/go-rod/rod/lib/js"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/rod/lib/utils"
)
// TraceType for logger.
type TraceType string
// String interface.
func (t TraceType) String() string {
return fmt.Sprintf("[%s]", string(t))
}
const (
// TraceTypeWaitRequestsIdle type.
TraceTypeWaitRequestsIdle TraceType = "wait requests idle"
// TraceTypeWaitRequests type.
TraceTypeWaitRequests TraceType = "wait requests"
// TraceTypeQuery type.
TraceTypeQuery TraceType = "query"
// TraceTypeWait type.
TraceTypeWait TraceType = "wait"
// TraceTypeInput type.
TraceTypeInput TraceType = "input"
)
// ServeMonitor starts the monitor server.
// The reason why not to use "chrome://inspect/#devices" is one target cannot be driven by multiple controllers.
func (b *Browser) ServeMonitor(host string) string {
u, mux, closeSvr := serve(host)
go func() {
<-b.ctx.Done()
utils.E(closeSvr())
}()
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
httHTML(w, assets.Monitor)
})
mux.HandleFunc("/api/pages", func(w http.ResponseWriter, _ *http.Request) {
res, err := proto.TargetGetTargets{}.Call(b) //nolint: contextcheck
utils.E(err)
list := []*proto.TargetTargetInfo{}
for _, info := range res.TargetInfos {
if info.Type == proto.TargetTargetInfoTypePage {
list = append(list, info)
}
}
w.WriteHeader(http.StatusOK)
utils.E(w.Write(utils.MustToJSONBytes(list)))
})
mux.HandleFunc("/page/", func(w http.ResponseWriter, _ *http.Request) {
httHTML(w, assets.MonitorPage)
})
mux.HandleFunc("/api/page/", func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]
info, err := b.pageInfo(proto.TargetTargetID(id)) //nolint: contextcheck
utils.E(err)
w.WriteHeader(http.StatusOK)
utils.E(w.Write(utils.MustToJSONBytes(info)))
})
mux.HandleFunc("/screenshot/", func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]
target := proto.TargetTargetID(id)
p := b.MustPageFromTargetID(target)
w.Header().Add("Content-Type", "image/png;")
utils.E(w.Write(p.MustScreenshot())) //nolint: contextcheck
})
return u
}
// check method and sleep if needed.
func (b *Browser) trySlowMotion() {
if b.slowMotion == 0 {
return
}
time.Sleep(b.slowMotion)
}
// ExposeHelpers helper functions to page's js context so that we can use the Devtools' console to debug them.
func (p *Page) ExposeHelpers(list ...*js.Function) {
p.MustEvaluate(evalHelper(&js.Function{
Name: "_" + utils.RandString(8), // use a random name so it won't hit the cache
Definition: "() => { window.rod = functions }",
Dependencies: list,
}))
}
// Overlay a rectangle on the main frame with specified message.
func (p *Page) Overlay(left, top, width, height float64, msg string) (remove func()) {
id := utils.RandString(8)
_, _ = p.root.Evaluate(evalHelper(js.Overlay,
id,
left,
top,
width,
height,
msg,
).ByPromise())
remove = func() {
_, _ = p.root.Evaluate(evalHelper(js.RemoveOverlay, id))
}
return
}
func (p *Page) tryTrace(typ TraceType, msg ...interface{}) func() {
if !p.browser.trace {
return func() {}
}
msg = append([]interface{}{typ}, msg...)
msg = append(msg, p)
p.browser.logger.Println(msg...)
return p.Overlay(0, 0, 500, 0, fmt.Sprint(msg))
}
func (p *Page) tryTraceQuery(opts *EvalOptions) func() {
if !p.browser.trace {
return func() {}
}
p.browser.logger.Println(TraceTypeQuery, opts, p)
msg := fmt.Sprintf("%s", html.EscapeString(opts.String()))
return p.Overlay(0, 0, 500, 0, msg)
}
func (p *Page) tryTraceReq(includes, excludes []string) func(map[proto.NetworkRequestID]string) {
if !p.browser.trace {
return func(map[proto.NetworkRequestID]string) {}
}
msg := map[string][]string{
"includes": includes,
"excludes": excludes,
}
p.browser.logger.Println(TraceTypeWaitRequestsIdle, msg, p)
cleanup := p.Overlay(0, 0, 500, 0, utils.MustToJSON(msg))
ch := make(chan map[string]string)
update := func(list map[proto.NetworkRequestID]string) {
clone := map[string]string{}
for k, v := range list {
clone[string(k)] = v
}
ch <- clone
}
go func() {
var waitList map[string]string
t := time.NewTicker(time.Second)
for {
select {
case <-p.ctx.Done():
t.Stop()
cleanup()
return
case waitList = <-ch:
case <-t.C:
p.browser.logger.Println(TraceTypeWaitRequests, p, waitList)
}
}
}()
return update
}
// Overlay msg on the element.
func (el *Element) Overlay(msg string) (removeOverlay func()) {
id := utils.RandString(8)
_, _ = el.Evaluate(evalHelper(js.ElementOverlay,
id,
msg,
).ByPromise())
removeOverlay = func() {
_, _ = el.Evaluate(evalHelper(js.RemoveOverlay, id))
}
return
}
func (el *Element) tryTrace(typ TraceType, msg ...interface{}) func() {
if !el.page.browser.trace {
return func() {}
}
msg = append([]interface{}{typ}, msg...)
msg = append(msg, el)
el.page.browser.logger.Println(msg...)
return el.Overlay(fmt.Sprint(msg))
}
func (m *Mouse) initMouseTracer() {
_, _ = m.page.Evaluate(evalHelper(js.InitMouseTracer, m.id, assets.MousePointer).ByPromise())
}
func (m *Mouse) updateMouseTracer() bool {
res, err := m.page.Evaluate(evalHelper(js.UpdateMouseTracer, m.id, m.pos.X, m.pos.Y))
if err != nil {
return true
}
return res.Value.Bool()
}
// Serve a port, if host is empty a random port will be used.
func serve(host string) (string, *http.ServeMux, func() error) {
if host == "" {
host = "127.0.0.1:0"
}
mux := http.NewServeMux()
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.WriteHeader(http.StatusBadRequest)
utils.E(json.NewEncoder(w).Encode(err))
}
}()
mux.ServeHTTP(w, r)
})}
l, err := net.Listen("tcp", host)
utils.E(err)
go func() { _ = srv.Serve(l) }()
url := "http://" + l.Addr().String()
return url, mux, srv.Close
}
golang-github-go-rod-rod-0.116.2/dev_helpers_test.go 0000664 0000000 0000000 00000004704 15206741465 0022264 0 ustar 00root root 0000000 0000000 package rod_test
import (
"testing"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/defaults"
"github.com/go-rod/rod/lib/js"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/rod/lib/utils"
"github.com/ysmood/gson"
)
func TestMonitor(t *testing.T) {
g := setup(t)
b := rod.New().MustConnect()
defer b.MustClose()
p := b.MustPage(g.blank()).MustWaitLoad()
b, cancel := b.WithCancel()
defer cancel()
host := b.Context(g.Context()).ServeMonitor("")
page := g.page.MustNavigate(host)
g.Has(page.MustElement("#targets a").MustParent().MustHTML(), string(p.TargetID))
page.MustNavigate(host + "/page/" + string(p.TargetID))
page.MustWait(`(id) => document.title.includes(id)`, p.TargetID)
img := g.Req("", host+"/screenshot").Bytes()
g.Gt(img.Len(), 10)
res := g.Req("", host+"/api/page/test")
g.Eq(400, res.StatusCode)
g.Eq(-32602, gson.New(res.Body).Get("code").Int())
}
func TestMonitorErr(t *testing.T) {
g := setup(t)
l := launcher.New()
u := l.MustLaunch()
defer l.Kill()
g.Panic(func() {
rod.New().Monitor("abc").ControlURL(u).MustConnect()
})
}
func TestTrace(t *testing.T) {
g := setup(t)
g.Eq(rod.TraceTypeInput.String(), "[input]")
var msg []interface{}
g.browser.Logger(utils.Log(func(list ...interface{}) { msg = list }))
g.browser.Trace(true).SlowMotion(time.Microsecond)
defer func() {
g.browser.Logger(rod.DefaultLogger)
g.browser.Trace(defaults.Trace).SlowMotion(defaults.Slow)
}()
p := g.page.MustNavigate(g.srcFile("fixtures/click.html")).MustWaitLoad()
g.Eq(rod.TraceTypeWait, msg[0])
g.Eq("load", msg[1])
g.Eq(p, msg[2])
el := p.MustElement("button")
el.MustClick()
g.Eq(rod.TraceTypeInput, msg[0])
g.Eq("left click", msg[1])
g.Eq(el, msg[2])
g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
_ = p.Mouse.MoveTo(proto.NewPoint(10, 10))
}
func TestTraceLogs(t *testing.T) {
g := setup(t)
g.browser.Logger(utils.LoggerQuiet)
g.browser.Trace(true)
defer func() {
g.browser.Logger(rod.DefaultLogger)
g.browser.Trace(defaults.Trace)
}()
p := g.page.MustNavigate(g.srcFile("fixtures/click.html"))
el := p.MustElement("button")
el.MustClick()
g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
p.Overlay(0, 0, 100, 30, "")
}
func TestExposeHelpers(t *testing.T) {
g := setup(t)
p := g.newPage(g.srcFile("fixtures/click.html"))
p.ExposeHelpers(js.ElementR)
g.Eq(p.MustElementByJS(`() => rod.elementR('button', 'click me')`).MustText(), "click me")
}
golang-github-go-rod-rod-0.116.2/element.go 0000664 0000000 0000000 00000046535 15206741465 0020366 0 ustar 00root root 0000000 0000000 package rod
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"time"
"github.com/go-rod/rod/lib/cdp"
"github.com/go-rod/rod/lib/input"
"github.com/go-rod/rod/lib/js"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/rod/lib/utils"
"github.com/ysmood/gson"
)
// Element implements these interfaces.
var (
_ proto.Client = &Element{}
_ proto.Contextable = &Element{}
_ proto.Sessionable = &Element{}
)
// Element represents the DOM element.
type Element struct {
Object *proto.RuntimeRemoteObject
e eFunc
ctx context.Context
sleeper func() utils.Sleeper
page *Page
}
// GetSessionID interface.
func (el *Element) GetSessionID() proto.TargetSessionID {
return el.page.SessionID
}
// String interface.
func (el *Element) String() string {
return fmt.Sprintf("<%s>", el.Object.Description)
}
// Page of the element.
func (el *Element) Page() *Page {
return el.page
}
// Focus sets focus on the specified element.
// Before the action, it will try to scroll to the element.
func (el *Element) Focus() error {
err := el.ScrollIntoView()
if err != nil {
return err
}
_, err = el.Evaluate(Eval(`() => this.focus()`).ByUser())
return err
}
// ScrollIntoView scrolls the current element into the visible area of the browser
// window if it's not already within the visible area.
func (el *Element) ScrollIntoView() error {
defer el.tryTrace(TraceTypeInput, "scroll into view")()
el.page.browser.trySlowMotion()
err := el.WaitStableRAF()
if err != nil {
return err
}
return proto.DOMScrollIntoViewIfNeeded{ObjectID: el.id()}.Call(el)
}
// Hover the mouse over the center of the element.
// Before the action, it will try to scroll to the element and wait until it's interactable.
func (el *Element) Hover() error {
pt, err := el.WaitInteractable()
if err != nil {
return err
}
return el.page.Context(el.ctx).Mouse.MoveTo(*pt)
}
// MoveMouseOut of the current element.
func (el *Element) MoveMouseOut() error {
shape, err := el.Shape()
if err != nil {
return err
}
box := shape.Box()
return el.page.Mouse.MoveTo(proto.NewPoint(box.X+box.Width, box.Y))
}
// Click will press then release the button just like a human.
// Before the action, it will try to scroll to the element, hover the mouse over it,
// wait until the it's interactable and enabled.
func (el *Element) Click(button proto.InputMouseButton, clickCount int) error {
err := el.Hover()
if err != nil {
return err
}
err = el.WaitEnabled()
if err != nil {
return err
}
defer el.tryTrace(TraceTypeInput, string(button)+" click")()
return el.page.Context(el.ctx).Mouse.Click(button, clickCount)
}
// Tap will scroll to the button and tap it just like a human.
// Before the action, it will try to scroll to the element and wait until it's interactable and enabled.
func (el *Element) Tap() error {
err := el.ScrollIntoView()
if err != nil {
return err
}
err = el.WaitEnabled()
if err != nil {
return err
}
pt, err := el.WaitInteractable()
if err != nil {
return err
}
defer el.tryTrace(TraceTypeInput, "tap")()
return el.page.Context(el.ctx).Touch.Tap(pt.X, pt.Y)
}
// Interactable checks if the element is interactable with cursor.
// The cursor can be mouse, finger, stylus, etc.
// If not interactable err will be ErrNotInteractable, such as when covered by a modal,.
func (el *Element) Interactable() (pt *proto.Point, err error) {
noPointerEvents, err := el.Eval(`() => getComputedStyle(this).pointerEvents === 'none'`)
if err != nil {
return nil, err
}
if noPointerEvents.Value.Bool() {
return nil, &NoPointerEventsError{el}
}
shape, err := el.Shape()
if err != nil {
return nil, err
}
pt = shape.OnePointInside()
if pt == nil {
err = &InvisibleShapeError{el}
return
}
scroll, err := el.page.root.Context(el.ctx).Eval(`() => ({ x: window.scrollX, y: window.scrollY })`)
if err != nil {
return
}
elAtPoint, err := el.page.Context(el.ctx).ElementFromPoint(
int(pt.X)+scroll.Value.Get("x").Int(),
int(pt.Y)+scroll.Value.Get("y").Int(),
)
if err != nil {
if errors.Is(err, cdp.ErrNodeNotFoundAtPos) {
err = &InvisibleShapeError{el}
}
return
}
isParent, err := el.ContainsElement(elAtPoint)
if err != nil {
return
}
if !isParent {
err = &CoveredError{elAtPoint}
}
return
}
// Shape of the DOM element content. The shape is a group of 4-sides polygons.
// A 4-sides polygon is not necessary a rectangle. 4-sides polygons can be apart from each other.
// For example, we use 2 4-sides polygons to describe the shape below:
//
// ____________ ____________
// / ___/ = /___________/ + _________
// /________/ /________/
func (el *Element) Shape() (*proto.DOMGetContentQuadsResult, error) {
return proto.DOMGetContentQuads{ObjectID: el.id()}.Call(el)
}
// Type is similar with Keyboard.Type.
// Before the action, it will try to scroll to the element and focus on it.
func (el *Element) Type(keys ...input.Key) error {
err := el.Focus()
if err != nil {
return err
}
return el.page.Context(el.ctx).Keyboard.Type(keys...)
}
// KeyActions is similar with Page.KeyActions.
// Before the action, it will try to scroll to the element and focus on it.
func (el *Element) KeyActions() (*KeyActions, error) {
err := el.Focus()
if err != nil {
return nil, err
}
return el.page.Context(el.ctx).KeyActions(), nil
}
// SelectText selects the text that matches the regular expression.
// Before the action, it will try to scroll to the element and focus on it.
func (el *Element) SelectText(regex string) error {
err := el.Focus()
if err != nil {
return err
}
defer el.tryTrace(TraceTypeInput, "select text: "+regex)()
el.page.browser.trySlowMotion()
_, err = el.Evaluate(evalHelper(js.SelectText, regex).ByUser())
return err
}
// SelectAllText selects all text
// Before the action, it will try to scroll to the element and focus on it.
func (el *Element) SelectAllText() error {
err := el.Focus()
if err != nil {
return err
}
defer el.tryTrace(TraceTypeInput, "select all text")()
el.page.browser.trySlowMotion()
_, err = el.Evaluate(evalHelper(js.SelectAllText).ByUser())
return err
}
// Input focuses on the element and input text to it.
// Before the action, it will scroll to the element, wait until it's visible, enabled and writable.
// To empty the input you can use something like
//
// el.SelectAllText().MustInput("")
func (el *Element) Input(text string) error {
err := el.Focus()
if err != nil {
return err
}
err = el.WaitEnabled()
if err != nil {
return err
}
err = el.WaitWritable()
if err != nil {
return err
}
err = el.page.Context(el.ctx).InsertText(text)
_, _ = el.Evaluate(evalHelper(js.InputEvent).ByUser())
return err
}
// InputTime focuses on the element and input time to it.
// Before the action, it will scroll to the element, wait until it's visible, enabled and writable.
// It will wait until the element is visible, enabled and writable.
func (el *Element) InputTime(t time.Time) error {
err := el.Focus()
if err != nil {
return err
}
err = el.WaitEnabled()
if err != nil {
return err
}
err = el.WaitWritable()
if err != nil {
return err
}
defer el.tryTrace(TraceTypeInput, "input "+t.String())()
_, err = el.Evaluate(evalHelper(js.InputTime, t.UnixNano()/1e6).ByUser())
return err
}
// InputColor focuses on the element and inputs a color string to it.
// Before the action, it will scroll to the element, wait until it's visible, enabled and writable.
func (el *Element) InputColor(color string) error {
err := el.Focus()
if err != nil {
return err
}
err = el.WaitEnabled()
if err != nil {
return err
}
err = el.WaitWritable()
if err != nil {
return err
}
defer el.tryTrace(TraceTypeInput, "input "+color)()
_, err = el.Evaluate(evalHelper(js.InputColor, color))
return err
}
// Blur removes focus from the element.
func (el *Element) Blur() error {
_, err := el.Evaluate(Eval("() => this.blur()").ByUser())
return err
}
// Select the children option elements that match the selectors.
// Before the action, it will scroll to the element, wait until it's visible.
// If no option matches the selectors, it will return [ErrElementNotFound].
func (el *Element) Select(selectors []string, selected bool, t SelectorType) error {
err := el.Focus()
if err != nil {
return err
}
defer el.tryTrace(TraceTypeInput, fmt.Sprintf(`select "%s"`, strings.Join(selectors, "; ")))()
el.page.browser.trySlowMotion()
res, err := el.Evaluate(evalHelper(js.Select, selectors, selected, t).ByUser())
if err != nil {
return err
}
if !res.Value.Bool() {
return &ElementNotFoundError{}
}
return nil
}
// Matches checks if the element can be selected by the css selector.
func (el *Element) Matches(selector string) (bool, error) {
res, err := el.Eval(`s => this.matches(s)`, selector)
if err != nil {
return false, err
}
return res.Value.Bool(), nil
}
// Attribute of the DOM object.
// Attribute vs Property:
// https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html
func (el *Element) Attribute(name string) (*string, error) {
attr, err := el.Eval("(n) => this.getAttribute(n)", name)
if err != nil {
return nil, err
}
if attr.Value.Nil() {
return nil, nil //nolint: nilnil
}
s := attr.Value.Str()
return &s, nil
}
// Property of the DOM object.
// Property vs Attribute:
// https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html
func (el *Element) Property(name string) (gson.JSON, error) {
prop, err := el.Eval("(n) => this[n]", name)
if err != nil {
return gson.New(nil), err
}
return prop.Value, nil
}
// Disabled checks if the element is disabled.
func (el *Element) Disabled() (bool, error) {
prop, err := el.Property("disabled")
if err != nil {
return false, err
}
return prop.Bool(), nil
}
// SetFiles of the current file input element.
func (el *Element) SetFiles(paths []string) error {
absPaths := utils.AbsolutePaths(paths)
defer el.tryTrace(TraceTypeInput, fmt.Sprintf("set files: %v", absPaths))()
el.page.browser.trySlowMotion()
err := proto.DOMSetFileInputFiles{
Files: absPaths,
ObjectID: el.id(),
}.Call(el)
return err
}
// Describe the current element. The depth is the maximum depth at which children should be retrieved, defaults to 1,
// use -1 for the entire subtree or provide an integer larger than 0.
// The pierce decides whether or not iframes and shadow roots should be traversed when returning the subtree.
// The returned [proto.DOMNode.NodeID] will always be empty,
// because NodeID is not stable (when [proto.DOMDocumentUpdated]
// is fired all NodeID on the page will be reassigned to another value)
// we don't recommend using the NodeID, instead, use the [proto.DOMBackendNodeID] to identify the element.
func (el *Element) Describe(depth int, pierce bool) (*proto.DOMNode, error) {
val, err := proto.DOMDescribeNode{ObjectID: el.id(), Depth: gson.Int(depth), Pierce: pierce}.Call(el)
if err != nil {
return nil, err
}
return val.Node, nil
}
// ShadowRoot returns the shadow root of this element.
func (el *Element) ShadowRoot() (*Element, error) {
node, err := el.Describe(1, false)
if err != nil {
return nil, err
}
// though now it's an array, w3c changed the spec of it to be a single.
if len(node.ShadowRoots) == 0 {
return nil, &NoShadowRootError{el}
}
id := node.ShadowRoots[0].BackendNodeID
shadowNode, err := proto.DOMResolveNode{BackendNodeID: id}.Call(el)
if err != nil {
return nil, err
}
return el.page.Context(el.ctx).ElementFromObject(shadowNode.Object)
}
// Frame creates a page instance that represents the iframe.
func (el *Element) Frame() (*Page, error) {
node, err := el.Describe(1, false)
if err != nil {
return nil, err
}
clone := *el.page
clone.FrameID = node.FrameID
clone.jsCtxID = new(proto.RuntimeRemoteObjectID)
clone.element = el
clone.sleeper = el.sleeper
return &clone, nil
}
// ContainsElement check if the target is equal or inside the element.
func (el *Element) ContainsElement(target *Element) (bool, error) {
res, err := el.Evaluate(evalHelper(js.ContainsElement, target.Object))
if err != nil {
return false, err
}
return res.Value.Bool(), nil
}
// Text that the element displays.
func (el *Element) Text() (string, error) {
str, err := el.Evaluate(evalHelper(js.Text))
if err != nil {
return "", err
}
return str.Value.String(), nil
}
// HTML of the element.
func (el *Element) HTML() (string, error) {
res, err := proto.DOMGetOuterHTML{ObjectID: el.Object.ObjectID}.Call(el)
if err != nil {
return "", err
}
return res.OuterHTML, nil
}
// Visible returns true if the element is visible on the page.
func (el *Element) Visible() (bool, error) {
res, err := el.Evaluate(evalHelper(js.Visible))
if err != nil {
return false, err
}
return res.Value.Bool(), nil
}
// WaitLoad for element like .
func (el *Element) WaitLoad() error {
defer el.tryTrace(TraceTypeWait, "load")()
_, err := el.Evaluate(evalHelper(js.WaitLoad).ByPromise())
return err
}
// WaitStable waits until no shape or position change for d duration.
// Be careful, d is not the max wait timeout, it's the least stable time.
// If you want to set a timeout you can use the [Element.Timeout] function.
func (el *Element) WaitStable(d time.Duration) error {
err := el.WaitVisible()
if err != nil {
return err
}
defer el.tryTrace(TraceTypeWait, "stable")()
shape, err := el.Shape()
if err != nil {
return err
}
t := time.NewTicker(d)
defer t.Stop()
for {
select {
case <-t.C:
case <-el.ctx.Done():
return el.ctx.Err()
}
current, err := el.Shape()
if err != nil {
return err
}
if reflect.DeepEqual(shape, current) {
break
}
shape = current
}
return nil
}
// WaitStableRAF waits until no shape or position change for 2 consecutive animation frames.
// If you want to wait animation that is triggered by JS not CSS, you'd better use [Element.WaitStable].
// About animation frame: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
func (el *Element) WaitStableRAF() error {
err := el.WaitVisible()
if err != nil {
return err
}
defer el.tryTrace(TraceTypeWait, "stable RAF")()
var shape *proto.DOMGetContentQuadsResult
page := el.page.Context(el.ctx)
for {
err = page.WaitRepaint()
if err != nil {
return err
}
current, err := el.Shape()
if err != nil {
return err
}
if reflect.DeepEqual(shape, current) {
break
}
shape = current
}
return nil
}
// WaitInteractable waits for the element to be interactable.
// It will try to scroll to the element on each try.
func (el *Element) WaitInteractable() (pt *proto.Point, err error) {
defer el.tryTrace(TraceTypeWait, "interactable")()
err = utils.Retry(el.ctx, el.sleeper(), func() (bool, error) {
// For lazy loading page the element can be outside of the viewport.
// If we don't scroll to it, it will never be available.
err := el.ScrollIntoView()
if err != nil {
return true, err
}
pt, err = el.Interactable()
if errors.Is(err, &CoveredError{}) {
return false, nil
}
return true, err
})
return
}
// Wait until the js returns true.
func (el *Element) Wait(opts *EvalOptions) error {
return el.page.Context(el.ctx).Sleeper(el.sleeper).Wait(opts.This(el.Object))
}
// WaitVisible until the element is visible.
func (el *Element) WaitVisible() error {
defer el.tryTrace(TraceTypeWait, "visible")()
return el.Wait(evalHelper(js.Visible))
}
// WaitEnabled until the element is not disabled.
// Doc for readonly: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly
func (el *Element) WaitEnabled() error {
defer el.tryTrace(TraceTypeWait, "enabled")()
return el.Wait(Eval(`() => !this.disabled`))
}
// WaitWritable until the element is not readonly.
// Doc for disabled: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled
func (el *Element) WaitWritable() error {
defer el.tryTrace(TraceTypeWait, "writable")()
return el.Wait(Eval(`() => !this.readonly`))
}
// WaitInvisible until the element invisible.
func (el *Element) WaitInvisible() error {
defer el.tryTrace(TraceTypeWait, "invisible")()
return el.Wait(evalHelper(js.Invisible))
}
// CanvasToImage get image data of a canvas.
// The default format is image/png.
// The default quality is 0.92.
// doc: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
func (el *Element) CanvasToImage(format string, quality float64) ([]byte, error) {
res, err := el.Eval(`(format, quality) => this.toDataURL(format, quality)`, format, quality)
if err != nil {
return nil, err
}
_, bin := parseDataURI(res.Value.Str())
return bin, nil
}
// Resource returns the "src" content of current element. Such as the jpg of
.
func (el *Element) Resource() ([]byte, error) {
src, err := el.Evaluate(evalHelper(js.Resource).ByPromise())
if err != nil {
return nil, err
}
return el.page.Context(el.ctx).GetResource(src.Value.String())
}
// BackgroundImage returns the css background-image of the element.
func (el *Element) BackgroundImage() ([]byte, error) {
res, err := el.Eval(`() => window.getComputedStyle(this).backgroundImage.replace(/^url\("/, '').replace(/"\)$/, '')`)
if err != nil {
return nil, err
}
u := res.Value.Str()
return el.page.Context(el.ctx).GetResource(u)
}
// Screenshot of the area of the element.
func (el *Element) Screenshot(format proto.PageCaptureScreenshotFormat, quality int) ([]byte, error) {
err := el.ScrollIntoView()
if err != nil {
return nil, err
}
opts := &proto.PageCaptureScreenshot{
Quality: gson.Int(quality),
Format: format,
}
bin, err := el.page.Context(el.ctx).Screenshot(false, opts)
if err != nil {
return nil, err
}
// so that it won't clip the css-transformed element
shape, err := el.Shape()
if err != nil {
return nil, err
}
box := shape.Box()
// TODO: proto.PageCaptureScreenshot has a Clip option, but it's buggy, so now we do in Go.
return utils.CropImage(bin, quality,
int(box.X),
int(box.Y),
int(box.Width),
int(box.Height),
)
}
// Release is a shortcut for [Page.Release] current element.
func (el *Element) Release() error {
return el.page.Context(el.ctx).Release(el.Object)
}
// Remove the element from the page.
func (el *Element) Remove() error {
_, err := el.Eval(`() => this.remove()`)
if err != nil {
return err
}
return el.Release()
}
// Call implements the [proto.Client].
func (el *Element) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) {
return el.page.Call(ctx, sessionID, methodName, params)
}
// Eval is a shortcut for [Element.Evaluate] with AwaitPromise, ByValue and AutoExp set to true.
func (el *Element) Eval(js string, params ...interface{}) (*proto.RuntimeRemoteObject, error) {
return el.Evaluate(Eval(js, params...).ByPromise())
}
// Evaluate is just a shortcut of [Page.Evaluate] with This set to current element.
func (el *Element) Evaluate(opts *EvalOptions) (*proto.RuntimeRemoteObject, error) {
return el.page.Context(el.ctx).Evaluate(opts.This(el.Object))
}
// Equal checks if the two elements are equal.
func (el *Element) Equal(elm *Element) (bool, error) {
res, err := el.Eval(`elm => this === elm`, elm.Object)
return res.Value.Bool(), err
}
func (el *Element) id() proto.RuntimeRemoteObjectID {
return el.Object.ObjectID
}
// GetXPath returns the xpath of the element.
func (el *Element) GetXPath(optimized bool) (string, error) {
str, err := el.Evaluate(evalHelper(js.GetXPath, optimized))
if err != nil {
return "", err
}
return str.Value.String(), nil
}
golang-github-go-rod-rod-0.116.2/element_test.go 0000664 0000000 0000000 00000056700 15206741465 0021420 0 ustar 00root root 0000000 0000000 package rod_test
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"image/color"
"image/png"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/cdp"
"github.com/go-rod/rod/lib/devices"
"github.com/go-rod/rod/lib/input"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/rod/lib/utils"
"github.com/ysmood/gson"
)
func TestGetElementPage(t *testing.T) {
g := setup(t)
el := g.page.MustNavigate(g.blank()).MustElement("html")
g.Eq(el.Page().SessionID, g.page.SessionID)
}
func TestClick(t *testing.T) {
g := setup(t)
p := g.page.MustNavigate(g.srcFile("fixtures/click.html"))
el := p.MustElement("button")
el.MustClick()
g.True(p.MustHas("[a=ok]"))
g.Panic(func() {
g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
el.MustClick()
})
g.Panic(func() {
g.mc.stubErr(8, proto.RuntimeCallFunctionOn{})
el.MustClick()
})
}
func TestClickWrapped(t *testing.T) {
g := setup(t)
p := g.page.MustNavigate(g.srcFile("fixtures/click-wrapped.html")).MustWaitLoad()
el := p.MustElement("#target")
shape := el.MustShape()
g.Len(shape.Quads, 2)
el.MustClick()
g.True(p.MustHas("[a=ok]"))
}
func TestTap(t *testing.T) {
g := setup(t)
page := g.newPage()
page.MustEmulate(devices.IPad).
MustNavigate(g.srcFile("fixtures/touch.html")).
MustWaitLoad()
el := page.MustElement("button")
el.MustTap()
g.True(page.MustHas("[tapped=true]"))
g.Panic(func() {
g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
el.MustTap()
})
g.Panic(func() {
g.mc.stubErr(1, proto.DOMScrollIntoViewIfNeeded{})
el.MustTap()
})
g.Panic(func() {
g.mc.stubErr(4, proto.RuntimeCallFunctionOn{})
el.MustTap()
})
g.Panic(func() {
g.mc.stubErr(7, proto.RuntimeCallFunctionOn{})
el.MustTap()
})
}
func TestInteractable(t *testing.T) {
g := setup(t)
p := g.page.MustNavigate(g.srcFile("fixtures/click.html"))
el := p.MustElement("button")
g.True(el.MustInteractable())
g.mc.stubErr(4, proto.RuntimeCallFunctionOn{})
g.Err(el.Interactable())
}
func TestNotInteractable(t *testing.T) {
g := setup(t)
p := g.page.MustNavigate(g.srcFile("fixtures/click.html"))
el := p.MustElement("button")
// cover the button with a green div
p.MustWaitLoad().MustEval(`() => {
let div = document.createElement('div')
div.style = 'position: absolute; left: 0; top: 0; width: 500px; height: 500px;'
document.body.append(div)
}`)
_, err := el.Interactable()
g.Has(err.Error(), "element covered by:
long-text-content-to-wrap