├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── question.md
├── pull_request_template.md
└── workflows
│ ├── check-examples.yml
│ ├── check-issues.yml
│ ├── check-revision.yml
│ ├── docker.yml
│ ├── test-linux.yml
│ └── test-other-platforms.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── README.md
├── browser.go
├── browser_test.go
├── context.go
├── dev_helpers.go
├── dev_helpers_test.go
├── element.go
├── element_test.go
├── error.go
├── examples_test.go
├── fixtures
├── README.md
├── add-script-tag.js
├── add-style-tag.css
├── alert.html
├── blank.html
├── canvas.html
├── chrome-extension
│ ├── main.js
│ └── manifest.json
├── click-iframe.html
├── click-iframes.html
├── click-wrapped.html
├── click.html
├── describe.html
├── design.sketch
├── drag.html
├── fetch.html
├── fonts.html
├── gen-fonts
│ └── main.go
├── icon.png
├── input.html
├── interactable.html
├── keys.html
├── mouse-pointer.svg
├── object-model.svg
├── open-page-subpage.html
├── open-page.html
├── prevent-close.html
├── resource.html
├── scroll.html
├── selector.html
├── shadow-dom.html
├── slow-render.html
├── touch.html
├── wait-stable.html
├── wait_elements.html
├── worker.html
└── worker.js
├── go.mod
├── go.sum
├── go.work
├── hijack.go
├── hijack_test.go
├── input.go
├── input_test.go
├── lab_test.go
├── lib
├── assets
│ ├── README.md
│ ├── assets.go
│ ├── generate
│ │ └── main.go
│ ├── monitor-page.html
│ └── monitor.html
├── benchmark
│ └── basic_test.go
├── cdp
│ ├── README.md
│ ├── client.go
│ ├── client_test.go
│ ├── error.go
│ ├── example_test.go
│ ├── fixtures
│ │ ├── basic.html
│ │ └── iframe.html
│ ├── format.go
│ ├── utils.go
│ ├── websocket.go
│ ├── websocket_private_test.go
│ └── websocket_test.go
├── defaults
│ ├── defaults.go
│ └── defaults_test.go
├── devices
│ ├── device.go
│ ├── generate
│ │ └── main.go
│ ├── list.go
│ ├── utils.go
│ └── utils_test.go
├── docker
│ ├── Dockerfile
│ ├── dev.Dockerfile
│ └── fonts-local.conf
├── examples
│ ├── anti-bot-detection
│ │ └── README.md
│ ├── compare-chromedp
│ │ ├── README.md
│ │ ├── click
│ │ │ └── main.go
│ │ ├── cookie
│ │ │ └── main.go
│ │ ├── emulate
│ │ │ └── main.go
│ │ ├── eval
│ │ │ └── main.go
│ │ ├── headers
│ │ │ └── main.go
│ │ ├── keys
│ │ │ └── main.go
│ │ ├── logic
│ │ │ └── main.go
│ │ ├── remote
│ │ │ └── main.go
│ │ ├── screenshot
│ │ │ └── main.go
│ │ ├── submit
│ │ │ └── main.go
│ │ ├── text
│ │ │ └── main.go
│ │ ├── upload
│ │ │ └── main.go
│ │ └── visible
│ │ │ └── main.go
│ ├── connect-browser
│ │ └── main.go
│ ├── custom-launch
│ │ └── main.go
│ ├── custom-websocket
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── main.go
│ ├── debug-deadlock
│ │ └── main.go
│ ├── disable-window-alert
│ │ └── main.go
│ ├── e2e-testing
│ │ ├── README.md
│ │ ├── calculator_test.go
│ │ ├── go.mod
│ │ └── setup_test.go
│ ├── launch-managed
│ │ └── main.go
│ ├── stripe
│ │ └── main.go
│ ├── translator
│ │ ├── README.md
│ │ └── main.go
│ └── use-rod-like-chrome-extension
│ │ └── main.go
├── input
│ ├── README.md
│ ├── keyboard.go
│ ├── keyboard_test.go
│ ├── keymap.go
│ ├── mac_comands.go
│ ├── mouse.go
│ └── mouse_test.go
├── js
│ ├── generate
│ │ └── main.go
│ ├── helper.go
│ ├── helper.js
│ └── js.go
├── launcher
│ ├── README.md
│ ├── browser.go
│ ├── example_test.go
│ ├── flags
│ │ └── flags.go
│ ├── launcher.go
│ ├── launcher_test.go
│ ├── load_test.go
│ ├── manager.go
│ ├── os_unix.go
│ ├── os_windows.go
│ ├── private_test.go
│ ├── revision.go
│ ├── revision
│ │ └── main.go
│ ├── rod-manager
│ │ └── main.go
│ ├── url_parser.go
│ └── utils.go
├── proto
│ ├── README.md
│ ├── a_interface.go
│ ├── a_interface_test.go
│ ├── a_patch.go
│ ├── a_utils.go
│ ├── a_utils_test.go
│ ├── accessibility.go
│ ├── animation.go
│ ├── audits.go
│ ├── background_service.go
│ ├── browser.go
│ ├── cache_storage.go
│ ├── cast.go
│ ├── console.go
│ ├── css.go
│ ├── database.go
│ ├── debugger.go
│ ├── definitions.go
│ ├── definitions_test.go
│ ├── device_orientation.go
│ ├── dom.go
│ ├── dom_debugger.go
│ ├── dom_snapshot.go
│ ├── dom_storage.go
│ ├── emulation.go
│ ├── event_breakpoints.go
│ ├── fetch.go
│ ├── generate
│ │ ├── main.go
│ │ ├── patch.go
│ │ ├── schema.go
│ │ └── utils.go
│ ├── headless_experimental.go
│ ├── heap_profiler.go
│ ├── indexed_db.go
│ ├── input.go
│ ├── inspector.go
│ ├── io.go
│ ├── layer_tree.go
│ ├── log.go
│ ├── media.go
│ ├── memory.go
│ ├── network.go
│ ├── overlay.go
│ ├── page.go
│ ├── performance.go
│ ├── performance_timeline.go
│ ├── profiler.go
│ ├── runtime.go
│ ├── schema.go
│ ├── security.go
│ ├── service_worker.go
│ ├── storage.go
│ ├── system_info.go
│ ├── target.go
│ ├── tethering.go
│ ├── tracing.go
│ ├── web_audio.go
│ └── web_authn.go
└── utils
│ ├── check-cov
│ └── main.go
│ ├── check-issue
│ ├── body-invalid.txt
│ ├── body.txt
│ ├── check_format.go
│ ├── check_version.go
│ ├── go.mod
│ ├── main.go
│ └── main_test.go
│ ├── docker
│ └── main.go
│ ├── get-browser
│ └── main.go
│ ├── lint
│ ├── eslint.yml
│ ├── main.go
│ ├── prefix.go
│ └── prettier.yml
│ ├── rename
│ └── main.go
│ ├── setup
│ └── main.go
│ ├── simple-check
│ └── main.go
│ ├── sleeper.go
│ ├── sleeper_test.go
│ ├── utils.go
│ └── utils_test.go
├── must.go
├── must_test.go
├── page.go
├── page_eval.go
├── page_eval_test.go
├── page_test.go
├── query.go
├── query_test.go
├── setup_test.go
├── states.go
└── utils.go
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at . All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [ysmood]
2 | custom:
3 | - 'https://git.io/JskPp' # WeChat, PayPal, Ethereum, Bitcoin
4 | - 'https://git.io/JsTXT' # Doc for Funding & Spending
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhance
6 | assignees: ''
7 | ---
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Title of your question.
4 | title: ''
5 | labels: question
6 | assignees: ''
7 | ---
8 |
9 | Rod Version: v0.0.0
10 |
11 | ## The code to demonstrate your question
12 |
13 | 1. Clone Rod to your local and cd to the repository:
14 |
15 | ```bash
16 | git clone https://github.com/go-rod/rod
17 | cd rod
18 | ```
19 |
20 | 1. Use your code to replace the content of function `TestLab` in file `lab_test.go`.
21 |
22 | 1. Test your code with: `go test -run TestLab`, make sure it fails as expected.
23 |
24 | 1. Replace ALL THE CONTENT under "The code to demonstrate your question" with your `TestLab` function, like below:
25 |
26 | ```go
27 | func TestLab(t *testing.T) {
28 | g := setup(t)
29 | g.Eq(1, 2) // the test should fail, here 1 doesn't equal 2
30 | }
31 | ```
32 |
33 | ## What you got
34 |
35 | Such as what error you see.
36 |
37 | ## What you expected to see
38 |
39 | Such as what you want to do.
40 |
41 | ## What have you tried to solve the question
42 |
43 | Such as after modifying some source code of Rod you are able to get rid of the problem.
44 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Development guide
2 |
3 | [Link](https://github.com/go-rod/rod/blob/master/.github/CONTRIBUTING.md)
4 |
5 | ## Test on local before making the PR
6 |
7 | ```bash
8 | go run ./lib/utils/simple-check
9 | ```
10 |
--------------------------------------------------------------------------------
/.github/workflows/check-examples.yml:
--------------------------------------------------------------------------------
1 | name: Check Examples
2 |
3 | on:
4 | schedule:
5 | - cron: '23 3 * * *'
6 |
7 | jobs:
8 | run:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/setup-go@v2
13 | with:
14 | go-version: 1.18
15 |
16 | - uses: actions/checkout@v2
17 |
18 | - run: |
19 | go test -run Example ./...
20 | go test ./lib/examples/e2e-testing
21 |
--------------------------------------------------------------------------------
/.github/workflows/check-issues.yml:
--------------------------------------------------------------------------------
1 | name: Check Issues
2 |
3 | on:
4 | issues:
5 | types: [opened, edited]
6 |
7 | jobs:
8 | run:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/setup-node@v2
13 | with:
14 | node-version: 16
15 |
16 | - uses: actions/setup-go@v2
17 | with:
18 | go-version: 1.18
19 |
20 | - uses: actions/checkout@v2
21 |
22 | - name: check
23 | env:
24 | ROD_GITHUB_ROBOT: ${{secrets.ROD_GITHUB_ROBOT}}
25 | run: cd lib/utils/check-issue && go run .
26 |
--------------------------------------------------------------------------------
/.github/workflows/check-revision.yml:
--------------------------------------------------------------------------------
1 | name: Check Revision
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 1 * *' # monthly
6 |
7 | jobs:
8 | run:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/setup-go@v2
13 | with:
14 | go-version: 1.18
15 |
16 | - uses: actions/checkout@v2
17 |
18 | - run: |
19 | go run ./lib/launcher/revision
20 | go generate
21 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | # When git master branch changes it will build a image based on the master branch, the tag of the image will be latest.
2 | # When a git semver tag is pushed it will build a image based on it, the tag will be the same as git's.
3 | # It will do nothing on other git events.
4 | # For the usage of the image, check lib/examples/launch-managed .
5 |
6 | name: Release docker image
7 |
8 | on: [push, pull_request]
9 |
10 | jobs:
11 | docker:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/setup-go@v2
16 | with:
17 | go-version: 1.18
18 |
19 | - uses: actions/checkout@v2
20 |
21 | - run: go run ./lib/utils/docker $GITHUB_REF
22 | env:
23 | DOCKER_TOKEN: ${{secrets.ROD_GITHUB_ROBOT}}
24 |
25 | - uses: actions/upload-artifact@v2
26 | with:
27 | name: review-fonts-docker
28 | path: tmp/fonts.pdf
29 |
30 | - uses: actions/upload-artifact@v2
31 | if: ${{ always() }}
32 | with:
33 | name: cdp-log-docker
34 | path: tmp/cdp-log
35 |
--------------------------------------------------------------------------------
/.github/workflows/test-linux.yml:
--------------------------------------------------------------------------------
1 | name: Test Linux
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 |
8 | pull_request:
9 |
10 | schedule:
11 | - cron: '17 5 * * *'
12 |
13 | env:
14 | GODEBUG: tracebackancestors=1000
15 |
16 | jobs:
17 | test-linux:
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/setup-node@v2
22 | with:
23 | node-version: 16
24 |
25 | - uses: actions/setup-go@v2
26 | with:
27 | go-version: 1.18
28 |
29 | - uses: actions/checkout@v2
30 |
31 | - run: go generate
32 |
33 | - run: go test -race -coverprofile=coverage.out ./...
34 |
35 | - run: go run ./lib/utils/check-cov
36 |
37 | - uses: actions/upload-artifact@v2
38 | if: ${{ always() }}
39 | with:
40 | name: cdp-log-linux
41 | path: |
42 | tmp/cdp-log
43 | coverage.out
44 |
--------------------------------------------------------------------------------
/.github/workflows/test-other-platforms.yml:
--------------------------------------------------------------------------------
1 | name: Test Other Platforms
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 |
8 | pull_request:
9 |
10 | env:
11 | GODEBUG: tracebackancestors=1000
12 |
13 | jobs:
14 | test-mac:
15 | runs-on: macos-latest
16 |
17 | steps:
18 | - uses: actions/setup-go@v2
19 | with:
20 | go-version: 1.18
21 |
22 | - uses: actions/checkout@v2
23 |
24 | - run: go test -timeout-each=2m
25 |
26 | - uses: actions/upload-artifact@v2
27 | if: ${{ always() }}
28 | with:
29 | name: cdp-log-macos
30 | path: tmp/cdp-log
31 |
32 | test-windows:
33 | runs-on: windows-latest
34 |
35 | steps:
36 | - uses: actions/setup-go@v2
37 | with:
38 | go-version: 1.18
39 |
40 | - uses: actions/checkout@v2
41 |
42 | - run: go test -timeout-each=2m
43 |
44 | - uses: actions/upload-artifact@v2
45 | if: ${{ always() }}
46 | with:
47 | name: cdp-log-windows
48 | path: tmp/cdp-log
49 |
50 | test-old-go:
51 | runs-on: ubuntu-latest
52 |
53 | steps:
54 | - uses: actions/setup-go@v2
55 | with:
56 | go-version: 1.13
57 |
58 | - uses: actions/checkout@v2
59 |
60 | # As long as the build works we don't have to run tests.
61 | - run: go build ./lib/examples/translator
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | node_modules/
3 | tmp/
4 |
5 | .git
6 | .dockerignore
7 | *.out
8 | *.test
9 | *.json
10 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | skip-dirs-use-default: false
3 |
4 | linters:
5 | enable:
6 | - gofmt
7 | - revive
8 | - gocyclo
9 | - misspell
10 | - bodyclose
11 |
12 | gocyclo:
13 | min-complexity: 15
14 |
15 | issues:
16 | exclude-use-default: false
17 |
18 | exclude-rules:
19 | # To support old golang version
20 | - path: lib/launcher/os_unix.go
21 | source: "// \\+build !windows"
22 | linters:
23 | - gofmt
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright 2019 Yad Smood
4 |
5 | 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:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | 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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 概览
2 |
3 | [](https://pkg.go.dev/github.com/go-rod/go-rod-chinese)
4 | [][discord room]
5 |
6 | ## [教程文档](https://go-rod.github.io/) | [英文 API 参考文档](https://pkg.go.dev/github.com/go-rod/rod?tab=doc) | [中文 API 参考文档](https://pkg.go.dev/github.com/go-rod/go-rod-chinese?tab=doc) | [项目管理](https://github.com/orgs/go-rod/projects/1) | [FAQ](https://go-rod.github.io/#/faq/README)
7 |
8 | Rod 是一个直接基于 [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol) 高级驱动程序。
9 | 它是为网页自动化和爬虫而设计的,既可用于高级应用开发也可用于低级应用开发,高级开发人员可以使用低级包和函数来轻松地定制或建立他们自己的Rod版本,高级函数只是建立Rod默认版本的例子。
10 |
11 | ## 特性
12 |
13 | - 链式上下文设计,直观地超时或取消长时间运行的任务
14 | - 自动等待元素准备就绪
15 | - 调试友好,自动输入跟踪,远程监控无头浏览器
16 | - 所有操作都是线程安全的
17 | - 自动查找或下载 [浏览器](lib/launcher)
18 | - 高级的辅助程序像 WaitStable, WaitRequestIdle, HijackRequests, WaitDownload,等
19 | - 两步式的 WaitEvent 设计,永远不会错过任何一个事件 ([工作原理](https://github.com/ysmood/goob))
20 | - 正确地处理嵌套的iframe或影子DOM
21 | - 崩溃后没有僵尸浏览器进程 ([工作原理](https://github.com/ysmood/leakless))
22 | - [CI](https://github.com/go-rod/rod/actions) 100% 的测试覆盖率
23 |
24 | ## 关于中文 API 参考文档的说明
25 |
26 | - 中文 API 参考文档中含有 `TODO` 的地方,表示目前的没有较好的翻译,如果有觉得很适合的翻译,请在中文仓库下提交 [issues](https://github.com/go-rod/go-rod-chinese/issues)/[discussions](https://github.com/go-rod/go-rod-chinese/discussions)
27 | - 翻译风格,翻译建议,翻译勘误,请在中文仓库下提交 [issues](https://github.com/go-rod/go-rod-chinese/issues)/[discussions](https://github.com/go-rod/go-rod-chinese/discussions)
28 | - 不建议将中文仓库的代码,使用在您的项目中,强烈建议使用[英文仓库](https://github.com/go-rod/rod)的代码。中文仓库仅供作为 API 文档中文版的参考
29 | - 关于API文档的翻译情况:对于底层库封装出来的接口已经全部翻译,底层库目前仅翻译了一些和功能业务相关的,例如:Network,Page等
30 | - 欢迎加入 rod 中文 API 参考文档的建设当中来
31 |
32 | ## 示例
33 |
34 | 首先请查看 [examples_test.go](examples_test.go), 然后查看 [examples](lib/examples) 文件夹.有关更详细的示例,请搜索单元测试。
35 | 例如 `HandleAuth`的使用, 你可以搜索所有 `*_test.go` 文件包含`HandleAuth`的,例如,使用 Github 在线搜索 [在仓库中搜索](https://github.com/go-rod/rod/search?q=HandleAuth&unscoped_q=HandleAuth)。
36 | 你也可以搜索 GitHub 的 [issues](https://github.com/go-rod/rod/issues) 或者 [discussions](https://github.com/go-rod/rod/discussions),这里记录了更多的使用示例。
37 |
38 | [这里](lib/examples/compare-chromedp) 是一个 rod 和 chromedp 的比较。
39 |
40 | 如果你有疑问,可以提 [issues](https://github.com/go-rod/rod/issues)/[discussions](https://github.com/go-rod/rod/discussions) 或者加入 [chat room][discord room]。
41 |
42 | ## 加入我们
43 |
44 | 我们非常欢迎你的帮助! 即使只是打开一个问题,提出一个问题,也可能大大帮助别人。
45 |
46 | 在你提出问题之前,请阅读 [如何聪明的提问](http://www.catb.org/~esr/faqs/smart-questions.html)。
47 |
48 | 我们使用 Github 项目来管理任务,你可以在[这里](https://github.com/orgs/go-rod/projects/1)看到这些问题的优先级和进展。
49 |
50 | 如果你想为项目作出贡献,请阅读 [Contributor Guide](.github/CONTRIBUTING.md)。
51 |
52 | [discord room]: https://discord.gg/CpevuvY
53 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | package rod
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/go-rod/rod/lib/utils"
8 | )
9 |
10 | type timeoutContextKey struct{}
11 | type timeoutContextVal struct {
12 | parent context.Context
13 | cancel context.CancelFunc
14 | }
15 |
16 | // Context 返回具有指定ctx的克隆,用于链式子操作
17 | func (b *Browser) Context(ctx context.Context) *Browser {
18 | newObj := *b
19 | newObj.ctx = ctx
20 | return &newObj
21 | }
22 |
23 | // GetContext 获取当前的ctx实例
24 | func (b *Browser) GetContext() context.Context {
25 | return b.ctx
26 | }
27 |
28 | // Timeout 返回一个克隆,其中包含所有链接子操作的指定总超时
29 | func (b *Browser) Timeout(d time.Duration) *Browser {
30 | ctx, cancel := context.WithTimeout(b.ctx, d)
31 | return b.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{b.ctx, cancel}))
32 | }
33 |
34 | // CancelTimeout 取消当前超时上下文,并返回具有父上下文的克隆
35 | func (b *Browser) CancelTimeout() *Browser {
36 | val := b.ctx.Value(timeoutContextKey{}).(*timeoutContextVal)
37 | val.cancel()
38 | return b.Context(val.parent)
39 | }
40 |
41 | // WithCancel 返回带有上下文取消函数的克隆
42 | func (b *Browser) WithCancel() (*Browser, func()) {
43 | ctx, cancel := context.WithCancel(b.ctx)
44 | return b.Context(ctx), cancel
45 | }
46 |
47 | // Sleeper 为链式子操作返回具有指定Sleeper的克隆
48 | func (b *Browser) Sleeper(sleeper func() utils.Sleeper) *Browser {
49 | newObj := *b
50 | newObj.sleeper = sleeper
51 | return &newObj
52 | }
53 |
54 | // Context 返回具有指定ctx的克隆,用于链式子操作
55 | func (p *Page) Context(ctx context.Context) *Page {
56 | newObj := *p
57 | newObj.ctx = ctx
58 | return &newObj
59 | }
60 |
61 | // GetContext 获取当前ctx实例
62 | func (p *Page) GetContext() context.Context {
63 | return p.ctx
64 | }
65 |
66 | // Timeout 返回一个克隆,其中包含所有链接子操作的指定总超时
67 | func (p *Page) Timeout(d time.Duration) *Page {
68 | ctx, cancel := context.WithTimeout(p.ctx, d)
69 | return p.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{p.ctx, cancel}))
70 | }
71 |
72 | // CancelTimeout 取消当前超时上下文,并返回具有父上下文的克隆
73 | func (p *Page) CancelTimeout() *Page {
74 | val := p.ctx.Value(timeoutContextKey{}).(*timeoutContextVal)
75 | val.cancel()
76 | return p.Context(val.parent)
77 | }
78 |
79 | // WithCancel 返回带有上下文取消函数的克隆
80 | func (p *Page) WithCancel() (*Page, func()) {
81 | ctx, cancel := context.WithCancel(p.ctx)
82 | return p.Context(ctx), cancel
83 | }
84 |
85 | // Sleeper 为链式子操作返回具有指定Sleeper的克隆
86 | func (p *Page) Sleeper(sleeper func() utils.Sleeper) *Page {
87 | newObj := *p
88 | newObj.sleeper = sleeper
89 | return &newObj
90 | }
91 |
92 | // Context 返回具有指定ctx的克隆,用于链式子操作
93 | func (el *Element) Context(ctx context.Context) *Element {
94 | newObj := *el
95 | newObj.ctx = ctx
96 | return &newObj
97 | }
98 |
99 | // GetContext 获取当前ctx的实例
100 | func (el *Element) GetContext() context.Context {
101 | return el.ctx
102 | }
103 |
104 | // Timeout 返回一个克隆,其中包含所有链接子操作的指定总超时
105 | func (el *Element) Timeout(d time.Duration) *Element {
106 | ctx, cancel := context.WithTimeout(el.ctx, d)
107 | return el.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{el.ctx, cancel}))
108 | }
109 |
110 | // CancelTimeout 取消当前超时上下文,并返回具有父上下文的克隆
111 | func (el *Element) CancelTimeout() *Element {
112 | val := el.ctx.Value(timeoutContextKey{}).(*timeoutContextVal)
113 | val.cancel()
114 | return el.Context(val.parent)
115 | }
116 |
117 | // WithCancel 返回带有上下文取消函数的克隆
118 | func (el *Element) WithCancel() (*Element, func()) {
119 | ctx, cancel := context.WithCancel(el.ctx)
120 | return el.Context(ctx), cancel
121 | }
122 |
123 | // Sleeper 为链式子操作返回具有指定Sleeper的克隆
124 | func (el *Element) Sleeper(sleeper func() utils.Sleeper) *Element {
125 | newObj := *el
126 | newObj.sleeper = sleeper
127 | return &newObj
128 | }
129 |
--------------------------------------------------------------------------------
/dev_helpers_test.go:
--------------------------------------------------------------------------------
1 | package rod_test
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/go-rod/rod"
8 | "github.com/go-rod/rod/lib/defaults"
9 | "github.com/go-rod/rod/lib/js"
10 | "github.com/go-rod/rod/lib/launcher"
11 | "github.com/go-rod/rod/lib/proto"
12 | "github.com/go-rod/rod/lib/utils"
13 | "github.com/ysmood/gson"
14 | )
15 |
16 | func TestMonitor(t *testing.T) {
17 | g := setup(t)
18 |
19 | b := rod.New().MustConnect()
20 | defer b.MustClose()
21 | p := b.MustPage(g.blank()).MustWaitLoad()
22 |
23 | b, cancel := b.WithCancel()
24 | defer cancel()
25 | host := b.Context(g.Context()).ServeMonitor("")
26 |
27 | page := g.page.MustNavigate(host)
28 | g.Has(page.MustElement("#targets a").MustParent().MustHTML(), string(p.TargetID))
29 |
30 | page.MustNavigate(host + "/page/" + string(p.TargetID))
31 | page.MustWait(`(id) => document.title.includes(id)`, p.TargetID)
32 |
33 | img := g.Req("", host+"/screenshot").Bytes()
34 | g.Gt(img.Len(), 10)
35 |
36 | res := g.Req("", host+"/api/page/test")
37 | g.Eq(400, res.StatusCode)
38 | g.Eq(-32602, gson.New(res.Body).Get("code").Int())
39 | }
40 |
41 | func TestMonitorErr(t *testing.T) {
42 | g := setup(t)
43 |
44 | l := launcher.New()
45 | u := l.MustLaunch()
46 | defer l.Kill()
47 |
48 | g.Panic(func() {
49 | rod.New().Monitor("abc").ControlURL(u).MustConnect()
50 | })
51 | }
52 |
53 | func TestTrace(t *testing.T) {
54 | g := setup(t)
55 |
56 | g.Eq(rod.TraceTypeInput.String(), "[input]")
57 |
58 | var msg []interface{}
59 | g.browser.Logger(utils.Log(func(list ...interface{}) { msg = list }))
60 | g.browser.Trace(true).SlowMotion(time.Microsecond)
61 | defer func() {
62 | g.browser.Logger(rod.DefaultLogger)
63 | g.browser.Trace(defaults.Trace).SlowMotion(defaults.Slow)
64 | }()
65 |
66 | p := g.page.MustNavigate(g.srcFile("fixtures/click.html")).MustWaitLoad()
67 |
68 | g.Eq(rod.TraceTypeWait, msg[0])
69 | g.Eq("load", msg[1])
70 | g.Eq(p, msg[2])
71 |
72 | el := p.MustElement("button")
73 | el.MustClick()
74 |
75 | g.Eq(rod.TraceTypeInput, msg[0])
76 | g.Eq("left click", msg[1])
77 | g.Eq(el, msg[2])
78 |
79 | g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
80 | _ = p.Mouse.Move(10, 10, 1)
81 | }
82 |
83 | func TestTraceLogs(t *testing.T) {
84 | g := setup(t)
85 |
86 | g.browser.Logger(utils.LoggerQuiet)
87 | g.browser.Trace(true)
88 | defer func() {
89 | g.browser.Logger(rod.DefaultLogger)
90 | g.browser.Trace(defaults.Trace)
91 | }()
92 |
93 | p := g.page.MustNavigate(g.srcFile("fixtures/click.html"))
94 | el := p.MustElement("button")
95 | el.MustClick()
96 |
97 | g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
98 | p.Overlay(0, 0, 100, 30, "")
99 | }
100 |
101 | func TestExposeHelpers(t *testing.T) {
102 | g := setup(t)
103 |
104 | p := g.newPage(g.srcFile("fixtures/click.html"))
105 | p.ExposeHelpers(js.ElementR)
106 |
107 | g.Eq(p.MustElementByJS(`() => rod.elementR('button', 'click me')`).MustText(), "click me")
108 | }
109 |
--------------------------------------------------------------------------------
/fixtures/README.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | The source code for all images under this folder are all come from file [design.sketch](design.sketch).
4 |
--------------------------------------------------------------------------------
/fixtures/add-script-tag.js:
--------------------------------------------------------------------------------
1 | window.n = 0
2 |
3 | window.count = () => {
4 | return window.n++
5 | }
6 |
--------------------------------------------------------------------------------
/fixtures/add-style-tag.css:
--------------------------------------------------------------------------------
1 | h4 {
2 | color: red;
3 | }
4 |
--------------------------------------------------------------------------------
/fixtures/alert.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/fixtures/blank.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/fixtures/canvas.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/fixtures/chrome-extension/main.js:
--------------------------------------------------------------------------------
1 | window.document.title = 'test-extension'
2 |
--------------------------------------------------------------------------------
/fixtures/chrome-extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 |
4 | "name": "test",
5 | "description": "Test extension",
6 | "version": "1.0",
7 | "content_scripts": [
8 | {
9 | "js": ["main.js"],
10 | "matches": [""]
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/fixtures/click-iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/fixtures/click-iframes.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/fixtures/click-wrapped.html:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
28 | long-text-content-to-wrap
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/fixtures/click.html:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 | Title
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/fixtures/describe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - coffee
5 | -
6 | tea
7 |
8 | - red tea
9 | - green tea
10 |
11 |
12 | - milk
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/fixtures/design.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-rod/go-rod-chinese/9e824259d90823736320f2ca06b112c7bf100a63/fixtures/design.sketch
--------------------------------------------------------------------------------
/fixtures/drag.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
21 |
22 |
23 |
28 | This div is draggable
29 |
30 |
31 |
32 |
33 |
34 |
122 |
123 |
--------------------------------------------------------------------------------
/fixtures/fetch.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
25 |
--------------------------------------------------------------------------------
/fixtures/gen-fonts/main.go:
--------------------------------------------------------------------------------
1 | // generates the fixtures/fonts.html for testing the fonts in docker.
2 | // Use the google translate to translate "test" into all the languages, print the result into a html page.
3 | // By reviewing the generated pdf we can find out what font is missing for a specific language.
4 |
5 | package main
6 |
7 | import (
8 | "fmt"
9 | "log"
10 |
11 | "github.com/go-rod/rod"
12 | "github.com/go-rod/rod/lib/launcher"
13 | "github.com/go-rod/rod/lib/utils"
14 | )
15 |
16 | func main() {
17 | url := launcher.New().MustLaunch()
18 | b := rod.New().ControlURL(url).MustConnect()
19 | defer b.MustClose()
20 |
21 | p := b.MustPage("https://translate.google.com/")
22 |
23 | p.MustElement("#source").MustInput("Test the google translate.")
24 |
25 | if p.MustHas(".tlid-dismiss-button") {
26 | p.MustElement(".tlid-dismiss-button").MustClick()
27 | }
28 |
29 | showList := p.MustElement(".tlid-open-target-language-list")
30 | list := p.MustElements(".language-list:nth-child(2) .language_list_section:nth-child(2) .language_list_item_language_name")
31 |
32 | html := ""
33 |
34 | for _, lang := range list {
35 | showList.MustClick()
36 | wait := p.MustWaitRequestIdle()
37 | lang.MustClick()
38 | wait()
39 | name := lang.MustText()
40 | result := p.MustElement(".tlid-translation").MustText()
41 | log.Println(name, result)
42 | html += fmt.Sprintf("%s | %s |
\n", name, result)
43 | }
44 |
45 | html = fmt.Sprintf(`
46 |
47 | This file is generated by "fixtures/gen-fonts"
48 |
49 | Test smileys: 😀 😁 😂 🤣 😃 😄 😅 😆 😉 😊 😋 😎 😍 😘 🥰 😗 😙 😚
50 | `,
53 | html,
54 | )
55 |
56 | utils.E(utils.OutputFile("fixtures/fonts.html", html))
57 | }
58 |
--------------------------------------------------------------------------------
/fixtures/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-rod/go-rod-chinese/9e824259d90823736320f2ca06b112c7bf100a63/fixtures/icon.png
--------------------------------------------------------------------------------
/fixtures/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/fixtures/interactable.html:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/fixtures/keys.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
--------------------------------------------------------------------------------
/fixtures/mouse-pointer.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/fixtures/open-page-subpage.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/fixtures/open-page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | open page
4 |
5 |
6 |
--------------------------------------------------------------------------------
/fixtures/prevent-close.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/fixtures/resource.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/fixtures/scroll.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/fixtures/selector.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 01
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/fixtures/shadow-dom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
--------------------------------------------------------------------------------
/fixtures/slow-render.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/fixtures/touch.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
47 |
48 |
--------------------------------------------------------------------------------
/fixtures/wait-stable.html:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/fixtures/wait_elements.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - list 1
5 | - list 2
6 | - list 3
7 | - list 4
8 | - list 5
9 |
10 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/fixtures/worker.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/fixtures/worker.js:
--------------------------------------------------------------------------------
1 | // echo message
2 | onmessage = (e) => postMessage(e.data)
3 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-rod/go-rod-chinese
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/ysmood/goob v0.4.0
7 | github.com/ysmood/got v0.31.3
8 | github.com/ysmood/gotrace v0.6.0
9 | github.com/ysmood/gson v0.7.1
10 | github.com/ysmood/leakless v0.8.0
11 | )
12 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
2 | github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
3 | github.com/ysmood/got v0.31.3 h1:UvvF+TDVsZLO7MSzm/Bd/H4HVp+7S5YwsxgdwaKq8uA=
4 | github.com/ysmood/got v0.31.3/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY=
5 | github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
6 | github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
7 | github.com/ysmood/gson v0.7.1 h1:zKL2MTGtynxdBdlZjyGsvEOZ7dkxaY5TH6QhAbTgz0Q=
8 | github.com/ysmood/gson v0.7.1/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
9 | github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak=
10 | github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
11 |
--------------------------------------------------------------------------------
/go.work:
--------------------------------------------------------------------------------
1 | go 1.18
2 |
3 | use (
4 | .
5 | ./lib/examples/custom-websocket
6 | ./lib/examples/e2e-testing
7 | ./lib/utils/check-issue
8 | )
9 |
--------------------------------------------------------------------------------
/lab_test.go:
--------------------------------------------------------------------------------
1 | package rod_test
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | // This is the template to demonstrate how to test Rod.
8 | func TestLab(t *testing.T) {
9 | g := setup(t)
10 | g.cancelTimeout() // Cancel timeout protection
11 |
12 | browser, page := g.browser, g.page
13 |
14 | // You can use the pre-launched g.browser for testing
15 | g.Eq(browser.MustVersion().ProtocolVersion, "1.3")
16 |
17 | // You can also use the pre-created g.page for testing
18 | page.MustNavigate(g.blank()).MustWaitLoad()
19 | g.Has(page.MustInfo().URL, "blank.html")
20 | }
21 |
--------------------------------------------------------------------------------
/lib/assets/README.md:
--------------------------------------------------------------------------------
1 | # Assets
2 |
3 | Static files for the project
4 |
--------------------------------------------------------------------------------
/lib/assets/generate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "path/filepath"
5 |
6 | "github.com/go-rod/rod/lib/utils"
7 | )
8 |
9 | var slash = filepath.FromSlash
10 |
11 | func main() {
12 | build := utils.S(`// generated by "lib/assets/generate"
13 |
14 | package assets
15 |
16 | // MousePointer for rod
17 | const MousePointer = {{.mousePointer}}
18 |
19 | // Monitor for rod
20 | const Monitor = {{.monitor}}
21 |
22 | // MonitorPage for rod
23 | const MonitorPage = {{.monitorPage}}
24 | `,
25 | "mousePointer", get("../../fixtures/mouse-pointer.svg"),
26 | "monitor", get("monitor.html"),
27 | "monitorPage", get("monitor-page.html"),
28 | )
29 |
30 | utils.E(utils.OutputFile(slash("lib/assets/assets.go"), build))
31 | }
32 |
33 | func get(path string) string {
34 | code, err := utils.ReadString(slash("lib/assets/" + path))
35 | utils.E(err)
36 | return utils.EscapeGoString(code)
37 | }
38 |
--------------------------------------------------------------------------------
/lib/assets/monitor-page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
43 |
44 |
45 |
51 |
52 |
60 |
61 |
62 |
63 |
64 |
103 |
104 |
--------------------------------------------------------------------------------
/lib/assets/monitor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Rod Monitor - Pages
4 |
31 |
32 |
33 | Choose a Page to Monitor
34 |
35 |
36 |
37 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/lib/benchmark/basic_test.go:
--------------------------------------------------------------------------------
1 | // Example run:
2 | // go test -bench . ./lib/benchmark
3 |
4 | package main_test
5 |
6 | import (
7 | "path/filepath"
8 | "testing"
9 |
10 | "github.com/go-rod/rod"
11 | "github.com/go-rod/rod/lib/launcher"
12 | "github.com/go-rod/rod/lib/utils"
13 | "github.com/ysmood/got"
14 | )
15 |
16 | func BenchmarkCleanup(b *testing.B) {
17 | u := got.New(b).Serve().Route("/", "", "page body").URL("/")
18 |
19 | b.RunParallel(func(pb *testing.PB) {
20 | for pb.Next() {
21 | launch := launcher.New().UserDataDir(filepath.Join("tmp", "cleanup", utils.RandString(8)))
22 | b.Cleanup(launch.Cleanup)
23 |
24 | url := launch.MustLaunch()
25 |
26 | browser := rod.New().ControlURL(url).MustConnect()
27 | b.Cleanup(browser.MustClose)
28 |
29 | browser.MustPage(u).MustClose()
30 | }
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/lib/cdp/README.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | This client is directly based on this [doc](https://chromedevtools.github.io/devtools-protocol/).
4 |
5 | You can treat it as a minimal example of how to use the DevTools Protocol, no complex abstraction.
6 |
7 | It's thread-safe, and context first.
8 |
9 | For basic usage, check this [file](example_test.go).
10 |
11 | For more info, check the unit tests.
12 |
--------------------------------------------------------------------------------
/lib/cdp/error.go:
--------------------------------------------------------------------------------
1 | package cdp
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // Error of the Response
8 | type Error struct {
9 | Code int `json:"code"`
10 | Message string `json:"message"`
11 | Data string `json:"data"`
12 | }
13 |
14 | // Error stdlib interface
15 | func (e *Error) Error() string {
16 | return fmt.Sprintf("%v", *e)
17 | }
18 |
19 | // Is stdlib interface
20 | func (e Error) Is(target error) bool {
21 | err, ok := target.(*Error)
22 | return ok && e == *err
23 | }
24 |
25 | // ErrCtxNotFound type
26 | var ErrCtxNotFound = &Error{
27 | Code: -32000,
28 | Message: "Cannot find context with specified id",
29 | }
30 |
31 | // ErrSessionNotFound type
32 | var ErrSessionNotFound = &Error{
33 | Code: -32001,
34 | Message: "Session with given id not found.",
35 | }
36 |
37 | // ErrSearchSessionNotFound type
38 | var ErrSearchSessionNotFound = &Error{
39 | Code: -32000,
40 | Message: "No search session with given id found",
41 | }
42 |
43 | // ErrCtxDestroyed type
44 | var ErrCtxDestroyed = &Error{
45 | Code: -32000,
46 | Message: "Execution context was destroyed.",
47 | }
48 |
49 | // ErrObjNotFound type
50 | var ErrObjNotFound = &Error{
51 | Code: -32000,
52 | Message: "Could not find object with given id",
53 | }
54 |
55 | // ErrNodeNotFoundAtPos type
56 | var ErrNodeNotFoundAtPos = &Error{
57 | Code: -32000,
58 | Message: "No node found at given location",
59 | }
60 |
--------------------------------------------------------------------------------
/lib/cdp/example_test.go:
--------------------------------------------------------------------------------
1 | package cdp_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/go-rod/rod/lib/cdp"
8 | "github.com/go-rod/rod/lib/launcher"
9 | "github.com/go-rod/rod/lib/proto"
10 | "github.com/go-rod/rod/lib/utils"
11 | "github.com/ysmood/gson"
12 | )
13 |
14 | func ExampleClient() {
15 | ctx := context.Background()
16 |
17 | // launch a browser
18 | url := launcher.New().MustLaunch()
19 |
20 | // create a controller
21 | client := cdp.New().Start(cdp.MustConnectWS(url))
22 |
23 | go func() {
24 | for range client.Event() {
25 | // you must consume the events
26 | }
27 | }()
28 |
29 | // Such as call this endpoint on the api doc:
30 | // https://chromedevtools.github.io/devtools-protocol/tot/Page#method-navigate
31 | // This will create a new tab and navigate to the test.com
32 | res, err := client.Call(ctx, "", "Target.createTarget", map[string]string{
33 | "url": "http://test.com",
34 | })
35 | utils.E(err)
36 |
37 | fmt.Println(len(gson.New(res).Get("targetId").Str()))
38 |
39 | // close browser by using the proto lib to encode json
40 | _ = proto.BrowserClose{}.Call(client)
41 |
42 | // Output: 32
43 | }
44 |
45 | func Example_customize_cdp_log() {
46 | ws := cdp.MustConnectWS(launcher.New().MustLaunch())
47 |
48 | cdp.New().
49 | Logger(utils.Log(func(args ...interface{}) {
50 | switch v := args[0].(type) {
51 | case *cdp.Request:
52 | fmt.Printf("id: %d", v.ID)
53 | }
54 | })).
55 | Start(ws)
56 | }
57 |
--------------------------------------------------------------------------------
/lib/cdp/fixtures/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | it works
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib/cdp/fixtures/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib/cdp/format.go:
--------------------------------------------------------------------------------
1 | package cdp
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-rod/rod/lib/utils"
7 | )
8 |
9 | func (req Request) String() string {
10 | return fmt.Sprintf(
11 | "=> #%d %s %s %s",
12 | req.ID,
13 | fSessionID(req.SessionID),
14 | req.Method,
15 | dump(req.Params),
16 | )
17 | }
18 |
19 | func (res Response) String() string {
20 | if res.Error != nil {
21 | return fmt.Sprintf(
22 | "<= #%d error: %s",
23 | res.ID,
24 | dump(res.Error),
25 | )
26 | }
27 | return fmt.Sprintf(
28 | "<= #%d %s",
29 | res.ID,
30 | dump(res.Result),
31 | )
32 | }
33 |
34 | func (e Event) String() string {
35 | return fmt.Sprintf(
36 | "<- %s %s %s",
37 | fSessionID(e.SessionID),
38 | e.Method,
39 | dump(e.Params),
40 | )
41 | }
42 |
43 | func fSessionID(s string) string {
44 | if s == "" {
45 | s = "00000000"
46 | }
47 | s = s[:8]
48 | return "@" + s
49 | }
50 |
51 | func dump(v interface{}) string {
52 | return utils.MustToJSON(v)
53 | }
54 |
--------------------------------------------------------------------------------
/lib/cdp/utils.go:
--------------------------------------------------------------------------------
1 | package cdp
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "net"
7 | "net/http"
8 |
9 | "github.com/go-rod/rod/lib/utils"
10 | )
11 |
12 | // Dialer interface for WebSocket connection
13 | type Dialer interface {
14 | DialContext(ctx context.Context, network, address string) (net.Conn, error)
15 | }
16 |
17 | // TODO: replace it with tls.Dialer once golang v1.15 is widely used.
18 | type tlsDialer struct{}
19 |
20 | func (d *tlsDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
21 | return tls.Dial(network, address, nil)
22 | }
23 |
24 | // MustConnectWS helper to make a websocket connection
25 | func MustConnectWS(wsURL string) WebSocketable {
26 | ws := &WebSocket{}
27 | utils.E(ws.Connect(context.Background(), wsURL, nil))
28 | return ws
29 | }
30 |
31 | // MustStartWithURL helper for ConnectURL
32 | func MustStartWithURL(ctx context.Context, u string, h http.Header) *Client {
33 | c, err := StartWithURL(ctx, u, h)
34 | utils.E(err)
35 | return c
36 | }
37 |
38 | // StartWithURL helper to connect to the u with the default websocket lib.
39 | func StartWithURL(ctx context.Context, u string, h http.Header) (*Client, error) {
40 | ws := &WebSocket{}
41 | err := ws.Connect(ctx, u, h)
42 | if err != nil {
43 | return nil, err
44 | }
45 | return New().Start(ws), nil
46 | }
47 |
--------------------------------------------------------------------------------
/lib/cdp/websocket_private_test.go:
--------------------------------------------------------------------------------
1 | package cdp
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "errors"
7 | "net"
8 | "net/url"
9 | "sync"
10 | "testing"
11 | "time"
12 |
13 | "github.com/ysmood/got"
14 | )
15 |
16 | var setup = got.Setup(nil)
17 |
18 | func TestWebSocketErr(t *testing.T) {
19 | g := setup(t)
20 |
21 | ws := WebSocket{}
22 | g.Err(ws.Connect(g.Context(), "://", nil))
23 |
24 | ws.Dialer = &net.Dialer{}
25 | ws.initDialer(nil)
26 |
27 | u, err := url.Parse("wss://no-exist")
28 | g.E(err)
29 | ws.Dialer = nil
30 | ws.initDialer(u)
31 |
32 | mc := &MockConn{}
33 | ws.conn = mc
34 | g.Err(ws.Send([]byte("test")))
35 |
36 | mc.errOnCount = 1
37 | mc.frame = []byte{0, 127, 1}
38 | ws.r = bufio.NewReader(mc)
39 | g.Err(ws.Read())
40 |
41 | mc.errOnCount = 1
42 | mc.frame = []byte{0}
43 | ws.r = bufio.NewReader(mc)
44 | g.Err(ws.Read())
45 |
46 | g.Err(ws.handshake(g.Timeout(0), nil, nil))
47 |
48 | mc.errOnCount = 1
49 | g.Err(ws.handshake(g.Context(), u, nil))
50 |
51 | tls := &tlsDialer{}
52 | g.Err(tls.DialContext(context.Background(), "", ""))
53 | }
54 |
55 | type MockConn struct {
56 | sync.Mutex
57 | errOnCount int
58 | frame []byte
59 | }
60 |
61 | func (c *MockConn) checkErr(d int) error {
62 | c.Lock()
63 | defer c.Unlock()
64 |
65 | if c.errOnCount == 0 {
66 | return errors.New("err")
67 | }
68 | c.errOnCount += d
69 | return nil
70 | }
71 |
72 | func (c *MockConn) Read(b []byte) (int, error) {
73 | if err := c.checkErr(-1); err != nil {
74 | return 0, err
75 | }
76 |
77 | return copy(b, c.frame), nil
78 | }
79 |
80 | func (c *MockConn) Write(b []byte) (int, error) {
81 | if err := c.checkErr(-1); err != nil {
82 | return 0, err
83 | }
84 | return len(b), nil
85 | }
86 |
87 | func (c *MockConn) Close() error {
88 | return c.checkErr(0)
89 | }
90 |
91 | func (c *MockConn) LocalAddr() net.Addr {
92 | return nil
93 | }
94 |
95 | func (c *MockConn) RemoteAddr() net.Addr {
96 | return nil
97 | }
98 |
99 | func (c *MockConn) SetDeadline(t time.Time) error {
100 | return nil
101 | }
102 |
103 | func (c *MockConn) SetReadDeadline(t time.Time) error {
104 | return nil
105 | }
106 |
107 | func (c *MockConn) SetWriteDeadline(t time.Time) error {
108 | return nil
109 | }
110 |
--------------------------------------------------------------------------------
/lib/cdp/websocket_test.go:
--------------------------------------------------------------------------------
1 | package cdp_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "path/filepath"
8 | "strings"
9 | "sync"
10 | "testing"
11 |
12 | "github.com/go-rod/rod/lib/cdp"
13 | "github.com/go-rod/rod/lib/launcher"
14 | "github.com/ysmood/got"
15 | "github.com/ysmood/gson"
16 | )
17 |
18 | func TestWebSocketLargePayload(t *testing.T) {
19 | g := setup(t)
20 |
21 | ctx := g.Context()
22 | client, id := newPage(ctx, g)
23 |
24 | res, err := client.Call(ctx, id, "Runtime.evaluate", map[string]interface{}{
25 | "expression": fmt.Sprintf(`"%s"`, strings.Repeat("a", 2*1024*1024)),
26 | "returnByValue": true,
27 | })
28 | g.E(err)
29 | g.Gt(res, 2*1024*1024) // 2MB
30 | }
31 |
32 | func ConcurrentCall(t *testing.T) {
33 | g := setup(t)
34 |
35 | ctx := g.Context()
36 | client, id := newPage(ctx, g)
37 |
38 | wg := sync.WaitGroup{}
39 | for i := 0; i < 30; i++ {
40 | wg.Add(1)
41 | go func() {
42 | res, err := client.Call(ctx, id, "Runtime.evaluate", map[string]interface{}{
43 | "expression": `10`,
44 | })
45 | g.Nil(err)
46 | g.Eq(string(res), "{\"result\":{\"type\":\"number\",\"value\":10,\"description\":\"10\"}}")
47 | wg.Done()
48 | }()
49 | }
50 | wg.Wait()
51 | }
52 |
53 | func TestWebSocketHeader(t *testing.T) {
54 | g := setup(t)
55 |
56 | s := g.Serve()
57 |
58 | wait := make(chan struct{})
59 | s.Mux.HandleFunc("/a", func(rw http.ResponseWriter, r *http.Request) {
60 | g.Eq(r.Header.Get("Test"), "header")
61 | g.Eq(r.Host, "test.com")
62 | g.Eq(r.URL.Query().Get("q"), "ok")
63 | close(wait)
64 | })
65 |
66 | ws := cdp.WebSocket{}
67 | err := ws.Connect(g.Context(), s.URL("/a?q=ok"), http.Header{
68 | "Host": {"test.com"},
69 | "Test": {"header"},
70 | })
71 | <-wait
72 |
73 | g.Eq(err.Error(), "websocket bad handshake: 200 OK. ")
74 | }
75 |
76 | func newPage(ctx context.Context, g got.G) (*cdp.Client, string) {
77 | l := launcher.New()
78 | g.Cleanup(l.Kill)
79 |
80 | client := cdp.New().Start(cdp.MustConnectWS(l.MustLaunch()))
81 |
82 | go func() {
83 | for range client.Event() {
84 | }
85 | }()
86 |
87 | file, err := filepath.Abs(filepath.FromSlash("fixtures/basic.html"))
88 | g.E(err)
89 |
90 | res, err := client.Call(ctx, "", "Target.createTarget", map[string]interface{}{
91 | "url": "file://" + file,
92 | })
93 | g.E(err)
94 |
95 | targetID := gson.New(res).Get("targetId").String()
96 |
97 | res, err = client.Call(ctx, "", "Target.attachToTarget", map[string]interface{}{
98 | "targetId": targetID,
99 | "flatten": true,
100 | })
101 | g.E(err)
102 |
103 | sessionID := gson.New(res).Get("sessionId").String()
104 |
105 | return client, sessionID
106 | }
107 |
108 | func TestDuplicatedConnectErr(t *testing.T) {
109 | g := setup(t)
110 |
111 | l := launcher.New()
112 | g.Cleanup(l.Kill)
113 |
114 | u := l.MustLaunch()
115 |
116 | ws := &cdp.WebSocket{}
117 | g.E(ws.Connect(g.Context(), u, nil))
118 |
119 | g.Panic(func() {
120 | _ = ws.Connect(g.Context(), u, nil)
121 | })
122 | }
123 |
--------------------------------------------------------------------------------
/lib/defaults/defaults_test.go:
--------------------------------------------------------------------------------
1 | package defaults
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/ysmood/got"
8 | )
9 |
10 | func TestBasic(t *testing.T) {
11 | g := got.T(t)
12 |
13 | Show = true
14 | Devtools = true
15 | URL = "test"
16 | Monitor = "test"
17 |
18 | ResetWith("")
19 | parse("")
20 | g.False(Show)
21 | g.False(Devtools)
22 | g.Eq("", Monitor)
23 | g.Eq("", URL)
24 | g.Eq(2978, LockPort)
25 |
26 | parse("show,devtools,trace,slow=2s,port=8080,dir=tmp," +
27 | "url=http://test.com,cdp,monitor,bin=/path/to/chrome," +
28 | "proxy=localhost:8080,lock=9981,",
29 | )
30 |
31 | g.True(Show)
32 | g.True(Devtools)
33 | g.True(Trace)
34 | g.Eq(2*time.Second, Slow)
35 | g.Eq("8080", Port)
36 | g.Eq("/path/to/chrome", Bin)
37 | g.Eq("tmp", Dir)
38 | g.Eq("http://test.com", URL)
39 | g.NotNil(CDP.Println)
40 | g.Eq(":0", Monitor)
41 | g.Eq("localhost:8080", Proxy)
42 | g.Eq(9981, LockPort)
43 |
44 | parse("monitor=:1234")
45 | g.Eq(":1234", Monitor)
46 |
47 | g.Panic(func() {
48 | parse("a")
49 | })
50 |
51 | g.Eq(try(func() { parse("slow=1") }), "invalid value for \"slow\": time: missing unit in duration \"1\" (learn format from https://golang.org/pkg/time/#ParseDuration)")
52 | }
53 |
54 | func try(fn func()) (err interface{}) {
55 | defer func() {
56 | err = recover()
57 | }()
58 |
59 | fn()
60 |
61 | return err
62 | }
63 |
64 | func TestParseFlag(t *testing.T) {
65 | g := got.T(t)
66 |
67 | Reset()
68 |
69 | parseFlag([]string{"-rod"})
70 | g.False(Show)
71 |
72 | parseFlag([]string{"-rod=show"})
73 | g.True(Show)
74 |
75 | Reset()
76 |
77 | parseFlag([]string{"-rod", "show"})
78 | g.True(Show)
79 | }
80 |
--------------------------------------------------------------------------------
/lib/devices/device.go:
--------------------------------------------------------------------------------
1 | package devices
2 |
3 | import (
4 | "github.com/go-rod/rod/lib/proto"
5 | "github.com/ysmood/gson"
6 | )
7 |
8 | // Device represents a emulated device.
9 | type Device struct {
10 | Capabilities []string
11 | UserAgent string
12 | AcceptLanguage string
13 | Screen Screen
14 | Title string
15 |
16 | landscape bool
17 | clear bool
18 | }
19 |
20 | // Screen represents the screen of a device.
21 | type Screen struct {
22 | DevicePixelRatio float64
23 | Horizontal ScreenSize
24 | Vertical ScreenSize
25 | }
26 |
27 | // ScreenSize represents the size of the screen.
28 | type ScreenSize struct {
29 | Width int
30 | Height int
31 | }
32 |
33 | // Landescape clones the device and set it to landscape mode
34 | func (device Device) Landescape() Device {
35 | d := device
36 | d.landscape = true
37 | return d
38 | }
39 |
40 | // MetricsEmulation config
41 | func (device Device) MetricsEmulation() *proto.EmulationSetDeviceMetricsOverride {
42 | if device.IsClear() {
43 | return nil
44 | }
45 |
46 | var screen ScreenSize
47 | var orientation *proto.EmulationScreenOrientation
48 | if device.landscape {
49 | screen = device.Screen.Horizontal
50 | orientation = &proto.EmulationScreenOrientation{
51 | Angle: 90,
52 | Type: proto.EmulationScreenOrientationTypeLandscapePrimary,
53 | }
54 | } else {
55 | screen = device.Screen.Vertical
56 | orientation = &proto.EmulationScreenOrientation{
57 | Angle: 0,
58 | Type: proto.EmulationScreenOrientationTypePortraitPrimary,
59 | }
60 | }
61 |
62 | return &proto.EmulationSetDeviceMetricsOverride{
63 | Width: screen.Width,
64 | Height: screen.Height,
65 | DeviceScaleFactor: device.Screen.DevicePixelRatio,
66 | ScreenOrientation: orientation,
67 | Mobile: has(device.Capabilities, "mobile"),
68 | }
69 | }
70 |
71 | // TouchEmulation config
72 | func (device Device) TouchEmulation() *proto.EmulationSetTouchEmulationEnabled {
73 | if device.IsClear() {
74 | return &proto.EmulationSetTouchEmulationEnabled{
75 | Enabled: false,
76 | }
77 | }
78 |
79 | return &proto.EmulationSetTouchEmulationEnabled{
80 | Enabled: has(device.Capabilities, "touch"),
81 | MaxTouchPoints: gson.Int(5),
82 | }
83 | }
84 |
85 | // UserAgentEmulation config
86 | func (device Device) UserAgentEmulation() *proto.NetworkSetUserAgentOverride {
87 | if device.IsClear() {
88 | return nil
89 | }
90 |
91 | return &proto.NetworkSetUserAgentOverride{
92 | UserAgent: device.UserAgent,
93 | AcceptLanguage: device.AcceptLanguage,
94 | }
95 | }
96 |
97 | // IsClear type
98 | func (device Device) IsClear() bool {
99 | return device.clear
100 | }
101 |
--------------------------------------------------------------------------------
/lib/devices/generate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http"
7 | "strings"
8 |
9 | "github.com/go-rod/rod/lib/utils"
10 | "github.com/ysmood/gson"
11 | )
12 |
13 | func main() {
14 | devices := getDeviceList()
15 |
16 | code := ``
17 | for _, d := range devices.Arr() {
18 | d = d.Get("device")
19 | name := d.Get("title").String()
20 |
21 | code += utils.S(`
22 |
23 | // {{.name}} device
24 | {{.name}} = Device{
25 | Title: "{{.title}}",
26 | Capabilities: {{.capabilities}},
27 | UserAgent: "{{.userAgent}}",
28 | AcceptLanguage: "en",
29 | Screen: Screen{
30 | DevicePixelRatio: {{.devicePixelRatio}},
31 | Horizontal: ScreenSize{
32 | Width: {{.horizontalWidth}},
33 | Height: {{.horizontalHeight}},
34 | },
35 | Vertical: ScreenSize{
36 | Width: {{.verticalWidth}},
37 | Height: {{.verticalHeight}},
38 | },
39 | },
40 | }`,
41 | "name", normalizeName(name),
42 | "title", name,
43 | "capabilities", toGoArr(d.Get("capabilities")),
44 | "userAgent", getUserAgent(d),
45 | "devicePixelRatio", d.Get("screen.device-pixel-ratio").Int(),
46 | "horizontalWidth", d.Get("screen.horizontal.width").Int(),
47 | "horizontalHeight", d.Get("screen.horizontal.height").Int(),
48 | "verticalWidth", d.Get("screen.vertical.width").Int(),
49 | "verticalHeight", d.Get("screen.vertical.height").Int(),
50 | )
51 | }
52 |
53 | code = utils.S(`// generated by "lib/devices/generate"
54 |
55 | package devices
56 |
57 | import (
58 | "github.com/go-rod/rod/lib/devices"
59 | )
60 |
61 | var (
62 | {{.code}}
63 | )
64 | `, "code", code)
65 |
66 | path := "./lib/devices/list.go"
67 | utils.E(utils.OutputFile(path, code))
68 |
69 | utils.Exec("gofmt -s -w", path)
70 | utils.Exec(
71 | "go run github.com/ysmood/golangci-lint@latest -- "+
72 | "run --no-config --fix --disable-all -E gofmt,goimports,misspell",
73 | path,
74 | )
75 | }
76 |
77 | func getDeviceList() gson.JSON {
78 | // we use the list from the web UI of devtools
79 | // TODO: We should keep update with their latest list, using hash id is a temp solution
80 | res, err := http.Get(
81 | "https://raw.githubusercontent.com/ChromeDevTools/devtools-frontend/c4e2fefe3327aa9fe5f4398a1baddb8726c230d5/front_end/emulated_devices/module.json",
82 | )
83 | utils.E(err)
84 | defer func() { _ = res.Body.Close() }()
85 |
86 | data, err := ioutil.ReadAll(res.Body)
87 | utils.E(err)
88 |
89 | return gson.New(data).Get("extensions")
90 | }
91 |
92 | func normalizeName(name string) string {
93 | name = strings.ReplaceAll(name, "/", "or")
94 |
95 | list := []string{}
96 | for _, s := range strings.Split(name, " ") {
97 | if len(s) > 1 {
98 | list = append(list, strings.ToUpper(s[0:1])+s[1:])
99 | } else {
100 | list = append(list, strings.ToUpper(s))
101 | }
102 | }
103 |
104 | return strings.Join(list, "")
105 | }
106 |
107 | func getUserAgent(val gson.JSON) string {
108 | ua := val.Get("user-agent").String()
109 | if ua == "" {
110 | return "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
111 | }
112 | ua = strings.ReplaceAll(ua, "%s", "87.0.4280.88")
113 | return ua
114 | }
115 |
116 | func toGoArr(val gson.JSON) string {
117 | list := []string{}
118 | for _, s := range val.Arr() {
119 | list = append(list, s.String())
120 | }
121 | return fmt.Sprintf("%#v", list)
122 | }
123 |
--------------------------------------------------------------------------------
/lib/devices/utils.go:
--------------------------------------------------------------------------------
1 | package devices
2 |
3 | // Clear is used to clear overrides
4 | var Clear = Device{clear: true}
5 |
6 | func has(arr []string, str string) bool {
7 | for _, item := range arr {
8 | if item == str {
9 | return true
10 | }
11 | }
12 | return false
13 | }
14 |
--------------------------------------------------------------------------------
/lib/devices/utils_test.go:
--------------------------------------------------------------------------------
1 | package devices_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/go-rod/rod/lib/devices"
7 | "github.com/ysmood/got"
8 | )
9 |
10 | func TestErr(t *testing.T) {
11 | as := got.New(t)
12 |
13 | v := devices.IPad.MetricsEmulation()
14 | touch := devices.IPad.TouchEmulation()
15 | as.Eq(768, v.Width)
16 | as.Eq(1024, v.Height)
17 | as.Eq(2, v.DeviceScaleFactor)
18 | as.Eq(0, v.ScreenOrientation.Angle)
19 | as.True(v.Mobile)
20 | as.True(touch.Enabled)
21 |
22 | v = devices.LaptopWithMDPIScreen.Landescape().MetricsEmulation()
23 | touch = devices.LaptopWithMDPIScreen.TouchEmulation()
24 | as.Eq(1280, v.Width)
25 | as.Eq(90, v.ScreenOrientation.Angle)
26 | as.False(v.Mobile)
27 | as.False(touch.Enabled)
28 |
29 | u := devices.IPad.UserAgentEmulation()
30 | as.Eq("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", u.UserAgent)
31 |
32 | as.Nil(devices.Clear.MetricsEmulation())
33 | as.False(devices.Clear.TouchEmulation().Enabled)
34 | as.Nil(devices.Clear.UserAgentEmulation())
35 | }
36 |
--------------------------------------------------------------------------------
/lib/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | # To build the image:
2 | # docker build -t ghcr.io/go-rod/rod -f lib/docker/Dockerfile .
3 | #
4 |
5 | # build rod-manager
6 | FROM golang AS go
7 |
8 | ARG goproxy="https://proxy.golang.org,direct"
9 |
10 | COPY . /rod
11 | WORKDIR /rod
12 | RUN go env -w GOPROXY=$goproxy
13 | RUN go build ./lib/launcher/rod-manager
14 | RUN go run ./lib/utils/get-browser
15 |
16 | FROM ubuntu:bionic
17 |
18 | ARG apt_sources="http://archive.ubuntu.com"
19 |
20 | RUN sed -i "s|http://archive.ubuntu.com|$apt_sources|g" /etc/apt/sources.list && \
21 | apt-get update && \
22 | apt-get install --no-install-recommends -y \
23 | # chromium dependencies
24 | libnss3 \
25 | libxss1 \
26 | libasound2 \
27 | libxtst6 \
28 | libgtk-3-0 \
29 | libgbm1 \
30 | ca-certificates \
31 | # fonts
32 | fonts-liberation fonts-noto-color-emoji fonts-noto-cjk \
33 | # timezone
34 | tzdata \
35 | # processs reaper
36 | dumb-init \
37 | # headful mode support, for example: $ xvfb-run chromium-browser --remote-debugging-port=9222
38 | xvfb \
39 | # cleanup
40 | && rm -rf /var/lib/apt/lists/*
41 |
42 | # processs reaper
43 | ENTRYPOINT ["dumb-init", "--"]
44 |
45 | COPY --from=go /root/.cache/rod /root/.cache/rod
46 | RUN ln -s /root/.cache/rod/browser/$(ls /root/.cache/rod/browser)/chrome-linux/chrome /usr/bin/chrome
47 |
48 | RUN touch /.dockerenv
49 |
50 | COPY --from=go /rod/rod-manager /usr/bin/
51 | CMD rod-manager
52 |
--------------------------------------------------------------------------------
/lib/docker/dev.Dockerfile:
--------------------------------------------------------------------------------
1 | # A docker image for rod development.
2 | # To build the image:
3 | # docker build -t ghcr.io/go-rod/rod:dev -f lib/docker/dev.Dockerfile .
4 |
5 | FROM ghcr.io/go-rod/rod
6 |
7 | ARG node="https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.xz"
8 | ARG golang="https://go.dev/dl/go1.18.linux-amd64.tar.gz"
9 | ARG apt_sources="http://archive.ubuntu.com"
10 |
11 | RUN sed -i "s|http://archive.ubuntu.com|$apt_sources|g" /etc/apt/sources.list && \
12 | apt-get update && apt-get install --no-install-recommends -y git curl xz-utils build-essential && \
13 | rm -rf /var/lib/apt/lists/*
14 |
15 | # install nodejs
16 | RUN curl -L $node > node.tar.xz && \
17 | tar -xf node.tar.xz && \
18 | mv node-* /usr/local/lib/.node && \
19 | rm node.tar.xz
20 |
21 | # install golang
22 | RUN curl -L $golang > golang.tar.gz && \
23 | tar -xf golang.tar.gz && \
24 | mv go /usr/local/lib/go && \
25 | rm golang.tar.gz
26 |
27 | ENV PATH="/usr/local/lib/.node/bin:/usr/local/lib/go/bin:/root/go/bin/:${PATH}"
28 |
29 | ENV GODEBUG="tracebackancestors=1000"
30 |
31 | # setup global git ignore
32 | RUN git config --global core.excludesfile ~/.gitignore_global
33 |
--------------------------------------------------------------------------------
/lib/docker/fonts-local.conf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | serif
7 |
8 | Noto Serif
9 | Noto Color Emoji
10 |
11 |
12 |
13 | sans-serif
14 |
15 | Noto Sans
16 | Noto Color Emoji
17 |
18 |
19 |
20 | sans
21 |
22 | Noto Sans
23 | Noto Color Emoji
24 |
25 |
26 |
27 | monospace
28 |
29 | Noto Sans Mono
30 | Noto Color Emoji
31 |
32 |
33 |
--------------------------------------------------------------------------------
/lib/examples/anti-bot-detection/README.md:
--------------------------------------------------------------------------------
1 | # Anti-bot-detection
2 |
3 | Check this [project](https://github.com/go-rod/stealth).
4 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/README.md:
--------------------------------------------------------------------------------
1 | # Rod comparison with chromedp
2 |
3 | chromedp is one of the most popular drivers for Devtools Protocol.
4 |
5 | To help developers who are familiar with chromedp to understand rod better we created side by side examples between rod and chromedp.
6 |
7 | To run an example:
8 |
9 | 1. clone rod
10 | 2. cd to the folder of an example, such as `cd lib/examples/compare-chromedp/click`
11 | 3. run `go run .`
12 |
13 | | rod | chromedp | Description |
14 | | -------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
15 | | [click](./click) | [click](https://github.com/chromedp/examples/blob/master/click) | use a selector to click on an element |
16 | | [cookie](./cookie) | [cookie](https://github.com/chromedp/examples/blob/master/cookie) | set a HTTP cookie on requests |
17 | | [emulate](./emulate) | [emulate](https://github.com/chromedp/examples/blob/master/emulate) | emulate a specific device such as an iPhone |
18 | | [eval](./eval) | [eval](https://github.com/chromedp/examples/blob/master/eval) | evaluate javascript and retrieve the result |
19 | | [headers](./headers) | [headers](https://github.com/chromedp/examples/blob/master/headers) | set a HTTP header on requests |
20 | | [keys](./keys) | [keys](https://github.com/chromedp/examples/blob/master/keys) | send key events to an element |
21 | | [logic](./logic) | [logic](https://github.com/chromedp/examples/blob/master/logic) | more complex logic beyond simple actions |
22 | | [remote](./remote) | [remote](https://github.com/chromedp/examples/blob/master/remote) | connect to an existing DevTools instance using a remote WebSocket URL |
23 | | [screenshot](./screenshot) | [screenshot](https://github.com/chromedp/examples/blob/master/screenshot) | take a screenshot of a specific element and of the entire browser viewport |
24 | | [submit](./submit) | [submit](https://github.com/chromedp/examples/blob/master/submit) | fill out and submit a form |
25 | | [text](./text) | [text](https://github.com/chromedp/examples/blob/master/text) | extract text from a specific element |
26 | | [upload](./upload) | [upload](https://github.com/chromedp/examples/blob/master/upload) | upload a file on a form |
27 | | [visible](./visible) | [visible](https://github.com/chromedp/examples/blob/master/visible) | wait until an element is visible |
28 |
29 | Occasionally, some of these examples may break if the specific websites these examples use get updated.
30 | We suggest you create an [issue](https://github.com/go-rod/rod/issues/new/choose).
31 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/click/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "github.com/go-rod/rod"
8 | )
9 |
10 | // This example demonstrates how to use a selector to click on an element.
11 | func main() {
12 | page := rod.New().
13 | MustConnect().
14 | Trace(true). // log useful info about what rod is doing
15 | Timeout(15 * time.Second).
16 | MustPage("https://pkg.go.dev/time/")
17 |
18 | // wait for footer element is visible (ie, page is loaded)
19 | page.MustElement(`body > footer`).MustWaitVisible()
20 |
21 | // find and click "Expand All" link
22 | page.MustElement(`#pkg-examples`).MustClick()
23 |
24 | // retrieve the value of the textarea
25 | example := page.MustElement(`#example-After textarea`).MustText()
26 |
27 | log.Printf("Go's time.After example:\n%s", example)
28 | }
29 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/cookie/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net"
8 | "net/http"
9 | "time"
10 |
11 | "github.com/go-rod/rod"
12 | "github.com/go-rod/rod/lib/proto"
13 | )
14 |
15 | // This example demonstrates how we can modify the cookies on a web page.
16 | func main() {
17 | expr := proto.TimeSinceEpoch(time.Now().Add(180 * 24 * time.Hour).Unix())
18 |
19 | page := rod.New().MustConnect().MustPage()
20 |
21 | page.MustSetCookies(&proto.NetworkCookieParam{
22 | Name: "cookie1",
23 | Value: "value1",
24 | Domain: "127.0.0.1",
25 | HTTPOnly: true,
26 | Expires: expr,
27 | }, &proto.NetworkCookieParam{
28 | Name: "cookie2",
29 | Value: "value2",
30 | Domain: "127.0.0.1",
31 | HTTPOnly: true,
32 | Expires: expr,
33 | })
34 |
35 | page.MustNavigate(cookieServer())
36 |
37 | // read network values
38 | for i, cookie := range page.MustCookies() {
39 | log.Printf("chrome cookie %d: %+v", i, cookie)
40 | }
41 |
42 | // chrome received cookies
43 | log.Printf("chrome received cookies: %s", page.MustElement(`#result`).MustText())
44 | }
45 |
46 | // cookieServer creates a simple HTTP server that logs any passed cookies.
47 | func cookieServer() string {
48 | l, _ := net.Listen("tcp4", "127.0.0.1:0")
49 | go func() {
50 | _ = http.Serve(l, http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
51 | cookies := req.Cookies()
52 | for i, cookie := range cookies {
53 | log.Printf("from %s, server received cookie %d: %v", req.RemoteAddr, i, cookie)
54 | }
55 | buf, err := json.MarshalIndent(req.Cookies(), "", " ")
56 | if err != nil {
57 | http.Error(res, err.Error(), http.StatusInternalServerError)
58 | return
59 | }
60 | _, _ = fmt.Fprintf(res, indexHTML, string(buf))
61 | }))
62 | }()
63 | return "http://" + l.Addr().String()
64 | }
65 |
66 | const (
67 | indexHTML = `
68 |
69 |
70 | %s
71 |
72 | `
73 | )
74 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/emulate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/go-rod/rod"
5 | "github.com/go-rod/rod/lib/devices"
6 | )
7 |
8 | func main() {
9 | page := rod.New().MustConnect().MustPage()
10 |
11 | // emulate iPhone 7 landscape
12 | err := page.Emulate(devices.IPhone6or7or8.Landescape())
13 | if err != nil {
14 | panic(err)
15 | }
16 |
17 | page.MustNavigate("https://www.whatsmyua.info/")
18 | page.MustScreenshot("screenshot1.png")
19 |
20 | // reset
21 | page.MustEmulate(devices.Clear)
22 |
23 | page.MustSetViewport(1920, 2000, 1, false)
24 | page.MustNavigate("https://www.whatsmyua.info/?a")
25 | page.MustScreenshot("screenshot2.png")
26 | }
27 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/eval/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/go-rod/rod"
7 | )
8 |
9 | // This example shows how we can use Eval to run scripts in the page.
10 | // Note: `this` in the eval function will refer to the element that Eval is
11 | // called on. This can be useful for things such as blurring elements.
12 | func main() {
13 | res := rod.New().MustConnect().
14 | MustPage("https://www.google.com/").
15 | MustElement(`input`).
16 | MustEval("() => Object.keys(window)")
17 |
18 | log.Printf("window object keys: %v", res)
19 | }
20 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/headers/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net"
8 | "net/http"
9 |
10 | "github.com/go-rod/rod"
11 | )
12 |
13 | // This example demonstrates how to set a HTTP header on requests.
14 | func main() {
15 | host := headerServer()
16 |
17 | page := rod.New().MustConnect().MustPage(host)
18 |
19 | page.MustSetExtraHeaders("X-Header", "my request header")
20 | page.MustNavigate(host)
21 | res := page.MustElement("#result").MustText()
22 |
23 | log.Printf("received headers: %s", res)
24 | }
25 |
26 | // headerServer is a simple HTTP server that displays the passed headers in the html.
27 | func headerServer() string {
28 | l, _ := net.Listen("tcp4", "127.0.0.1:0")
29 | go func() {
30 | _ = http.Serve(l, http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
31 | buf, err := json.MarshalIndent(req.Header, "", " ")
32 | if err != nil {
33 | http.Error(res, err.Error(), http.StatusInternalServerError)
34 | return
35 | }
36 | _, _ = fmt.Fprintf(res, indexHTML, string(buf))
37 | }))
38 | }()
39 | return "http://" + l.Addr().String()
40 | }
41 |
42 | const indexHTML = `
43 |
44 |
45 | %s
46 |
47 | `
48 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/keys/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 | "net/http"
8 |
9 | "github.com/go-rod/rod"
10 | "github.com/go-rod/rod/lib/input"
11 | )
12 |
13 | // This example demonstrates how to send key events to an element.
14 | func main() {
15 | page := rod.New().MustConnect().MustPage(testServer())
16 |
17 | val1 := page.MustElement("#input1").MustText()
18 | val2 := page.MustElement("#textarea1").MustInput("\b\b\n\naoeu\n\ntest1\n\nblah2\n\n\t\t\t\b\bother box!\t\ntest4").MustText()
19 | val3 := page.MustElement("#input2").MustInput("test3").MustText()
20 | val4 := page.MustElement("#select1").MustType(input.ArrowDown, input.ArrowDown).MustProperty("value").Str()
21 |
22 | log.Printf("#input1 value: %s", val1)
23 | log.Printf("#textarea1 value: %s", val2)
24 | log.Printf("#input2 value: %s", val3)
25 | log.Printf("#select1 value: %s", val4)
26 | }
27 |
28 | // testServer is a simple HTTP server that displays elements and inputs
29 | func testServer() string {
30 | l, _ := net.Listen("tcp4", "127.0.0.1:0")
31 | go func() {
32 | _ = http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33 | _, _ = fmt.Fprint(w, indexHTML)
34 | }))
35 | }()
36 | return "http://" + l.Addr().String()
37 | }
38 |
39 | const indexHTML = `
40 |
41 |
42 | example
43 |
44 |
45 |
50 |
65 |
66 | `
67 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/logic/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "github.com/go-rod/rod"
8 | )
9 |
10 | // On awesome-go page, finding the specified section sect,
11 | // and retrieving the associated projects from the page.
12 | func main() {
13 | page := rod.New().MustConnect().Timeout(time.Second * 15).MustPage("https://github.com/avelino/awesome-go")
14 |
15 | section := page.MustElementR("p", "Selenium and browser control tools").MustNext()
16 |
17 | // query children elements of an element
18 | projects := section.MustElements("li")
19 |
20 | for _, project := range projects {
21 | link := project.MustElement("a")
22 | log.Printf(
23 | "project %s (%s): '%s'",
24 | link.MustText(),
25 | link.MustProperty("href"),
26 | project.MustText(),
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/remote/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 |
7 | "github.com/go-rod/rod"
8 | )
9 |
10 | var flagDevToolWsURL = flag.String("devtools-ws-url", "", "DevTools WebSocket URL")
11 |
12 | // This example demonstrates how to connect to an existing Chrome DevTools
13 | // instance using a remote WebSocket URL.
14 | func main() {
15 | flag.Parse()
16 | if *flagDevToolWsURL == "" {
17 | log.Fatal("must specify -devtools-ws-url")
18 | }
19 |
20 | page := rod.New().ControlURL(*flagDevToolWsURL).MustConnect().MustPage("https://duckduckgo.com")
21 |
22 | page.MustElement("#logo_homepage_link").MustWaitVisible()
23 |
24 | log.Println("Body of duckduckgo.com starts with:")
25 | log.Println(page.MustHTML()[0:100])
26 | }
27 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/screenshot/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io/ioutil"
5 |
6 | "github.com/go-rod/rod"
7 | "github.com/go-rod/rod/lib/proto"
8 | "github.com/ysmood/gson"
9 | )
10 |
11 | // This example demonstrates how to take a screenshot of a specific element and
12 | // of the entire browser viewport, as well as using `kit`
13 | // to store it into a file.
14 | func main() {
15 | browser := rod.New().MustConnect()
16 |
17 | // capture screenshot of an element
18 | browser.MustPage("https://google.com").MustElement("body div").MustScreenshot("elementScreenshot.png")
19 |
20 | // capture entire browser viewport, returning jpg with quality=90
21 | buf, err := browser.MustPage("https://brank.as/").Screenshot(true, &proto.PageCaptureScreenshot{
22 | Format: proto.PageCaptureScreenshotFormatJpeg,
23 | Quality: gson.Int(90),
24 | })
25 | if err != nil {
26 | panic(err)
27 | }
28 |
29 | err = ioutil.WriteFile("fullScreenshot.png", buf, 0644)
30 | if err != nil {
31 | panic(err)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/submit/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "strings"
6 |
7 | "github.com/go-rod/rod"
8 | "github.com/go-rod/rod/lib/input"
9 | )
10 |
11 | //This example demonstrates how to fill out and submit a form.
12 | func main() {
13 | page := rod.New().MustConnect().MustPage("https://github.com/search")
14 |
15 | page.MustElement(`input[name=q]`).MustWaitVisible().MustInput("chromedp").MustType(input.Enter)
16 |
17 | res := page.MustElementR("a", "chromedp").MustParent().MustParent().MustNext().MustText()
18 |
19 | log.Printf("got: `%s`", strings.TrimSpace(res))
20 | }
21 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/text/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "strings"
6 |
7 | "github.com/go-rod/rod"
8 | )
9 |
10 | // This example demonstrates how to extract text from a specific element.
11 | func main() {
12 | page := rod.New().MustConnect().MustPage("https://pkg.go.dev/time")
13 |
14 | res := page.MustElement("#pkg-overview").MustParent().MustText()
15 | log.Println(strings.TrimSpace(res))
16 | }
17 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/upload/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "net"
8 | "net/http"
9 | "os"
10 |
11 | "github.com/go-rod/rod"
12 | )
13 |
14 | // This example demonstrates how to upload a file on a form.
15 | func main() {
16 | host := uploadServer()
17 |
18 | page := rod.New().MustConnect().MustPage(host)
19 |
20 | page.MustElement(`input[name="upload"]`).MustSetFiles("./main.go")
21 | page.MustElement(`input[name="submit"]`).MustClick()
22 |
23 | log.Printf(
24 | "original size: %d, upload size: %s",
25 | size("./main.go"),
26 | page.MustElement("#result").MustText(),
27 | )
28 | }
29 |
30 | // get some info about the file
31 | func size(file string) int {
32 | fi, err := os.Stat(file)
33 | if err != nil {
34 | panic(err)
35 | }
36 | return int(fi.Size())
37 | }
38 |
39 | func uploadServer() string {
40 | // create http server and result channel
41 | mux := http.NewServeMux()
42 | mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
43 | _, _ = fmt.Fprint(res, uploadHTML)
44 | })
45 | mux.HandleFunc("/upload", func(res http.ResponseWriter, req *http.Request) {
46 | f, _, err := req.FormFile("upload")
47 | if err != nil {
48 | http.Error(res, err.Error(), http.StatusBadRequest)
49 | return
50 | }
51 | defer func() { _ = f.Close() }()
52 |
53 | buf, err := ioutil.ReadAll(f)
54 | if err != nil {
55 | http.Error(res, err.Error(), http.StatusBadRequest)
56 | return
57 | }
58 |
59 | _, _ = fmt.Fprintf(res, resultHTML, len(buf))
60 | })
61 | l, _ := net.Listen("tcp4", "127.0.0.1:0")
62 | go func() { _ = http.Serve(l, mux) }()
63 | return "http://" + l.Addr().String()
64 | }
65 |
66 | const (
67 | uploadHTML = `
68 |
69 |
70 |
74 |
75 | `
76 |
77 | resultHTML = `
78 |
79 |
80 | %d
81 |
82 | `
83 | )
84 |
--------------------------------------------------------------------------------
/lib/examples/compare-chromedp/visible/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 | "net/http"
8 |
9 | "github.com/go-rod/rod"
10 | )
11 |
12 | func main() {
13 | page := rod.New().MustConnect().MustPage(testServer())
14 | page.MustEval(makeVisibleScript)
15 |
16 | log.Printf("waiting 3s for box to become visible")
17 |
18 | page.MustElement("#box1").MustWaitVisible()
19 | log.Printf(">>>>>>>>>>>>>>>>>>>> BOX1 IS VISIBLE")
20 |
21 | page.MustElement("#box2").MustWaitVisible()
22 | log.Printf(">>>>>>>>>>>>>>>>>>>> BOX2 IS VISIBLE")
23 | }
24 |
25 | const (
26 | makeVisibleScript = `() => setTimeout(function() {
27 | document.querySelector('#box1').style.display = '';
28 | }, 3000)`
29 | )
30 |
31 | // testServer is a simple HTTP server that serves a static html page.
32 | func testServer() string {
33 | l, _ := net.Listen("tcp4", "127.0.0.1:0")
34 | go func() {
35 | _ = http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
36 | _, _ = fmt.Fprint(w, indexHTML)
37 | }))
38 | }()
39 | return "http://" + l.Addr().String()
40 | }
41 |
42 | const indexHTML = `
43 |
44 |
45 | example
46 |
47 |
48 |
53 |
68 |
69 | `
70 |
--------------------------------------------------------------------------------
/lib/examples/connect-browser/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-rod/rod"
7 | "github.com/go-rod/rod/lib/launcher"
8 | )
9 |
10 | // To manually launch a browser
11 | func main() {
12 | // Launch your local browser first:
13 | //
14 | // chrome --headless --remote-debugging-port=9222
15 | //
16 | // Or use docker:
17 | //
18 | // docker run -p 9222:9222 ghcr.io/go-rod/rod chrome --headless --no-sandbox --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0
19 | //
20 | u := launcher.MustResolveURL("")
21 |
22 | browser := rod.New().ControlURL(u).MustConnect()
23 |
24 | fmt.Println(
25 | browser.MustPage("https://mdn.dev/").MustEval("() => document.title"),
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/lib/examples/custom-launch/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-rod/rod"
7 | "github.com/go-rod/rod/lib/launcher"
8 | )
9 |
10 | func main() {
11 | l := launcher.New()
12 |
13 | // For more info: https://pkg.go.dev/github.com/go-rod/rod/lib/launcher
14 | u := l.MustLaunch()
15 |
16 | browser := rod.New().ControlURL(u).MustConnect()
17 |
18 | page := browser.MustPage("http://example.com")
19 |
20 | fmt.Println(page.MustInfo().Title)
21 | }
22 |
--------------------------------------------------------------------------------
/lib/examples/custom-websocket/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-rod/rod/lib/examples/custom-websocket
2 |
3 | go 1.18
4 |
5 | require github.com/gobwas/ws v1.1.0
6 |
7 | require (
8 | github.com/gobwas/httphead v0.1.0 // indirect
9 | github.com/gobwas/pool v0.2.1 // indirect
10 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/lib/examples/custom-websocket/go.sum:
--------------------------------------------------------------------------------
1 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
2 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
3 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
4 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
5 | github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
6 | github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
7 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d h1:MiWWjyhUzZ+jvhZvloX6ZrUsdEghn8a64Upd8EMHglE=
8 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
9 |
--------------------------------------------------------------------------------
/lib/examples/custom-websocket/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "net"
8 |
9 | "github.com/go-rod/rod"
10 | "github.com/go-rod/rod/lib/cdp"
11 | "github.com/go-rod/rod/lib/launcher"
12 | "github.com/gobwas/ws"
13 | "github.com/gobwas/ws/wsutil"
14 | )
15 |
16 | func main() {
17 | w := NewWebSocket(launcher.New().MustLaunch())
18 |
19 | client := cdp.New().Start(w)
20 |
21 | p := rod.New().Client(client).MustConnect().MustPage("http://example.com")
22 |
23 | fmt.Println(p.MustInfo().Title)
24 | }
25 |
26 | // WebSocket is a custom websocket that uses gobwas/ws as the transport layer.
27 | type WebSocket struct {
28 | conn net.Conn
29 | }
30 |
31 | // NewWebSocket ...
32 | func NewWebSocket(u string) *WebSocket {
33 | conn, _, _, err := ws.Dial(context.Background(), u)
34 | if err != nil {
35 | log.Fatal(err)
36 | }
37 | return &WebSocket{conn}
38 | }
39 |
40 | // Send ...
41 | func (w *WebSocket) Send(b []byte) error {
42 | return wsutil.WriteClientText(w.conn, b)
43 | }
44 |
45 | // Read ...
46 | func (w *WebSocket) Read() ([]byte, error) {
47 | return wsutil.ReadServerText(w.conn)
48 | }
49 |
--------------------------------------------------------------------------------
/lib/examples/debug-deadlock/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-rod/rod"
7 | "github.com/ysmood/gotrace"
8 | )
9 |
10 | // This example shows how to detect the hanging points of golang code.
11 | // It's actually a general way to debug any golang project.
12 | func main() {
13 | defer checkLock()()
14 |
15 | go yourCodeHere()
16 | }
17 |
18 | // Put your code here, press Ctrl+C when you feel the program is hanging.
19 | // Read each goroutine's stack that is related to your own code logic.
20 | func yourCodeHere() {
21 | page := rod.New().MustConnect().MustPage("http://mdn.dev")
22 | go page.MustElement("not-exists")
23 | }
24 |
25 | // For this example you will find something like this below:
26 |
27 | /*
28 | goroutine 7 [select]:
29 | github.com/go-rod/rod.(*Page).MustElement(0xc00037e000, 0xc00063a0f0, 0x1, 0x1, 0x0)
30 | rod/must.go:425 +0x4d
31 | created by main.yourCodeHere
32 | rod/lib/examples/debug-deadlock/main.go:22 +0xb8
33 | */
34 |
35 | // From it we know the line 22 is blocking the code.
36 |
37 | func checkLock() func() {
38 | ctx := gotrace.Signal()
39 | ignored := gotrace.IgnoreCurrent()
40 | return func() {
41 | fmt.Println(gotrace.Wait(ctx, ignored))
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/examples/disable-window-alert/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/go-rod/rod"
8 | "github.com/go-rod/rod/lib/utils"
9 | )
10 |
11 | func main() {
12 | go serve()
13 |
14 | browser := rod.New().MustConnect()
15 | defer browser.MustClose()
16 |
17 | // Creating a Page Object
18 | page := browser.MustPage()
19 |
20 | // Evaluates given script in every frame upon creation
21 | // Disable all alerts by making window.alert no-op.
22 | page.MustEvalOnNewDocument(`window.alert = () => {}`)
23 |
24 | // Navigate to the website you want to visit
25 | page.MustNavigate("http://localhost:8080")
26 |
27 | fmt.Println(page.MustElement("script").MustText())
28 | }
29 |
30 | const testPage = ``
31 |
32 | // mock a server
33 | func serve() {
34 | mux := http.NewServeMux()
35 | mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
36 | utils.E(fmt.Fprint(res, testPage))
37 | })
38 | utils.E(http.ListenAndServe(":8080", mux))
39 | }
40 |
--------------------------------------------------------------------------------
/lib/examples/e2e-testing/README.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | This is a sample project to demonstrate how to use Rod to setup an end-to-end testing (e2e testing) project.
4 | The test cases will run in parallel.
5 |
6 | Use standard Go commands to test the project, such as run `go test` to execute all tests.
7 |
8 | ## Debugging
9 |
10 | Same as the tutorial here: [See what's under the hood](https://go-rod.github.io/#/get-started/README?id=see-what39s-under-the-hood)
11 |
--------------------------------------------------------------------------------
/lib/examples/e2e-testing/calculator_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "testing"
4 |
5 | // test case: 1 + 2 = 3
6 | func TestAdd(t *testing.T) {
7 | g := setup(t)
8 |
9 | p := g.page("https://ahfarmer.github.io/calculator")
10 |
11 | p.MustElementR("button", "1").MustClick()
12 | p.MustElementR("button", `^\+$`).MustClick()
13 | p.MustElementR("button", "2").MustClick()
14 | p.MustElementR("button", "=").MustClick()
15 |
16 | // assert the result with t.Eq
17 | g.Eq(p.MustElement(".component-display").MustText(), "3")
18 | }
19 |
20 | // test case: 2 * 3 = 6
21 | func TestMultiple(t *testing.T) {
22 | g := setup(t)
23 |
24 | p := g.page("https://ahfarmer.github.io/calculator")
25 |
26 | // use for-loop to click each button
27 | for _, regex := range []string{"2", "x", "3", "="} {
28 | p.MustElementR("button", regex).MustClick()
29 | }
30 |
31 | g.Eq(p.MustElement(".component-display").MustText(), "6")
32 | }
33 |
--------------------------------------------------------------------------------
/lib/examples/e2e-testing/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-rod/rod/lib/examples/e2e-testing
2 |
3 | go 1.18
4 |
--------------------------------------------------------------------------------
/lib/examples/e2e-testing/setup_test.go:
--------------------------------------------------------------------------------
1 | // This is the setup file for this test suite.
2 |
3 | package main
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/go-rod/rod"
9 | "github.com/ysmood/got"
10 | )
11 |
12 | // test context
13 | type G struct {
14 | got.G
15 |
16 | browser *rod.Browser
17 | }
18 |
19 | // setup for tests
20 | var setup = func() func(t *testing.T) G {
21 | browser := rod.New().MustConnect()
22 |
23 | return func(t *testing.T) G {
24 | t.Parallel() // run each test concurrently
25 |
26 | return G{got.New(t), browser}
27 | }
28 | }()
29 |
30 | // a helper function to create an incognito page
31 | func (g G) page(url string) *rod.Page {
32 | page := g.browser.MustIncognito().MustPage(url)
33 | g.Cleanup(page.MustClose)
34 | return page
35 | }
36 |
--------------------------------------------------------------------------------
/lib/examples/launch-managed/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-rod/rod"
7 | "github.com/go-rod/rod/lib/launcher"
8 | "github.com/go-rod/rod/lib/utils"
9 | )
10 |
11 | func main() {
12 | // This example is to launch a browser remotely, not connect to a running browser remotely,
13 | // to connect to a running browser check the "../connect-browser" example.
14 | // Rod provides a docker image for beginers, run the below to start a launcher.Manager:
15 | //
16 | // docker run -p 7317:7317 ghcr.io/go-rod/rod
17 | //
18 | // For more information, check the doc of launcher.Manager
19 | l := launcher.MustNewManaged("")
20 |
21 | // You can also set any flag remotely before you launch the remote browser.
22 | // Available flags: https://peter.sh/experiments/chromium-command-line-switches
23 | l.Set("disable-gpu").Delete("disable-gpu")
24 |
25 | // Launch with headful mode
26 | l.Headless(false).XVFB("--server-num=5", "--server-args=-screen 0 1600x900x16")
27 |
28 | browser := rod.New().Client(l.MustClient()).MustConnect()
29 |
30 | // You may want to start a server to watch the screenshots of the remote browser.
31 | launcher.Open(browser.ServeMonitor(""))
32 |
33 | fmt.Println(
34 | browser.MustPage("https://mdn.dev/").MustEval("() => document.title"),
35 | )
36 |
37 | utils.Pause()
38 | }
39 |
--------------------------------------------------------------------------------
/lib/examples/stripe/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "net/http"
7 |
8 | "github.com/go-rod/rod"
9 | "github.com/ysmood/gson"
10 | )
11 |
12 | // An example to handle stripe 3DS callback.
13 | func main() {
14 | page := rod.New().MustConnect().MustPage(getRedirectURL())
15 |
16 | // Get the button from the nested iframes
17 | frame01 := page.MustElement("div iframe").MustFrame()
18 | frame02 := frame01.MustElement("#challengeFrame").MustFrame()
19 | btn := frame02.MustElementR("button", "COMPLETE").MustWaitStable()
20 |
21 | wait := frame02.MustWaitRequestIdle()
22 | btn.MustClick()
23 | wait()
24 | }
25 |
26 | // Create a card payment that requires Visa's confirmation
27 | func getRedirectURL() string {
28 | token := post(
29 | "/tokens", "card[number]=4000000000003220&card[exp_month]=7&card[exp_year]=2025&card[cvc]=314",
30 | ).Get("id").Str()
31 |
32 | return post(
33 | "/payment_intents",
34 | "amount=100¤cy=usd&payment_method_data[type]=card&confirm=true&return_url=https%3A%2F%2Fmdn.dev"+
35 | "&payment_method_data[card][token]="+token,
36 | ).Get("next_action.redirect_to_url.url").Str()
37 | }
38 |
39 | func post(path, body string) gson.JSON {
40 | req, _ := http.NewRequest(http.MethodPost, "https://api.stripe.com/v1"+path, bytes.NewBufferString(body))
41 | req.Header.Add("Authorization", "Bearer sk_test_4eC39HqLyjWDarjtT1zdp7dc")
42 | res, _ := http.DefaultClient.Do(req)
43 | if res != nil {
44 | defer func() { _ = res.Body.Close() }()
45 | }
46 | data, _ := ioutil.ReadAll(res.Body)
47 | return gson.New(data)
48 | }
49 |
--------------------------------------------------------------------------------
/lib/examples/translator/README.md:
--------------------------------------------------------------------------------
1 | # Run the Example
2 |
3 | ```bash
4 | go run . 茶叶蛋
5 | ```
6 |
--------------------------------------------------------------------------------
/lib/examples/translator/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "strings"
8 |
9 | "github.com/go-rod/rod"
10 | )
11 |
12 | func main() {
13 | flag.Parse()
14 |
15 | // get the commandline arguments
16 | source := strings.TrimSpace(strings.Join(flag.Args(), " "))
17 | if source == "" {
18 | log.Fatal("usage: go run main.go -- 'This is the phrase to translate to Spanish.'")
19 | }
20 |
21 | browser := rod.New().MustConnect()
22 |
23 | page := browser.MustPage("https://translate.google.com/?sl=auto&tl=es&op=translate")
24 |
25 | el := page.MustElement(`textarea[aria-label="Source text"]`)
26 |
27 | wait := page.MustWaitRequestIdle("https://accounts.google.com")
28 | el.MustInput(source)
29 | wait()
30 |
31 | result := page.MustElement("[role=region] span[lang]").MustText()
32 |
33 | fmt.Println(result)
34 | }
35 |
--------------------------------------------------------------------------------
/lib/examples/use-rod-like-chrome-extension/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/go-rod/rod"
11 | "github.com/go-rod/rod/lib/launcher"
12 | "github.com/go-rod/rod/lib/proto"
13 | "github.com/go-rod/rod/lib/utils"
14 | "github.com/ysmood/gson"
15 | )
16 |
17 | // For example, when you log into your github account, and you want to reuse the login session for automation task.
18 | // You can use this example to achieve such functionality. Rod will be just like your browser extension.
19 | func main() {
20 | // Make sure you have closed your browser completely, UserMode can't control a browser that is not launched by it.
21 | // Launches a new browser with the "new user mode" option, and returns the URL to control that browser.
22 | wsURL := launcher.NewUserMode().MustLaunch()
23 |
24 | browser := rod.New().ControlURL(wsURL).MustConnect().NoDefaultDevice()
25 |
26 | // Run a extension. Here we created a link previewer extension as an example.
27 | // With this extension, whenever you hover on a link a preview of the linked page will popup.
28 | linkPreviewer(browser)
29 |
30 | browser.MustPage()
31 |
32 | waitExit()
33 | }
34 |
35 | func linkPreviewer(browser *rod.Browser) {
36 | // Create a headless browser to generate preview of links on background.
37 | previewer := rod.New().MustConnect()
38 | previewer.MustSetCookies(browser.MustGetCookies()...) // share cookies
39 | pool := rod.NewPagePool(5)
40 | create := func() *rod.Page { return previewer.MustPage() }
41 |
42 | go browser.EachEvent(func(e *proto.TargetTargetCreated) {
43 | if e.TargetInfo.Type != proto.TargetTargetInfoTypePage {
44 | return
45 | }
46 | page := browser.MustPageFromTargetID(e.TargetInfo.TargetID)
47 |
48 | // Inject js to every new page
49 | page.MustEvalOnNewDocument(js)
50 |
51 | // Expose a function to the page to provide preview
52 | page.MustExpose("getPreview", func(url gson.JSON) (interface{}, error) {
53 | p := pool.Get(create)
54 | defer pool.Put(p)
55 | p.MustNavigate(url.Str())
56 | return base64.StdEncoding.EncodeToString(p.MustScreenshot()), nil
57 | })
58 | })()
59 | }
60 |
61 | var jsLib = get("https://unpkg.com/@popperjs/core@2") + get("https://unpkg.com/tippy.js@6")
62 |
63 | var js = fmt.Sprintf(`window.addEventListener('load', () => {
64 | %s
65 |
66 | function setup(el) {
67 | el.classList.add('x-set')
68 | tippy(el, {onShow: async (it) => {
69 | if (it.props.content.src) return
70 | let img = document.createElement('img')
71 | img.style.width = '400px'
72 | img.src = "data:image/png;base64," + await getPreview(el.href)
73 | it.setContent(img)
74 | }, content: 'loading...', maxWidth: 500})
75 | }
76 |
77 | (function check() {
78 | Array.from(document.querySelectorAll('a:not(.x-set)')).forEach(setup)
79 | setTimeout(check, 1000)
80 | })()
81 | })`, jsLib)
82 |
83 | func get(u string) string {
84 | res, err := http.Get(u)
85 | utils.E(err)
86 | defer func() { _ = res.Body.Close() }()
87 | b, err := ioutil.ReadAll(res.Body)
88 | utils.E(err)
89 | return string(b)
90 | }
91 |
92 | func waitExit() {
93 | fmt.Println("Press Enter to exit...")
94 | utils.E(fmt.Scanln())
95 | os.Exit(0)
96 | }
97 |
--------------------------------------------------------------------------------
/lib/input/README.md:
--------------------------------------------------------------------------------
1 | # input
2 |
3 | A lib to help encode inputs.
4 |
--------------------------------------------------------------------------------
/lib/input/keyboard.go:
--------------------------------------------------------------------------------
1 | package input
2 |
3 | import (
4 | "github.com/go-rod/rod/lib/proto"
5 | "github.com/ysmood/gson"
6 | )
7 |
8 | // Modifier values
9 | const (
10 | ModifierAlt = 1
11 | ModifierControl = 2
12 | ModifierMeta = 4
13 | ModifierShift = 8
14 | )
15 |
16 | // Key symbol
17 | type Key rune
18 |
19 | // keyMap for key description
20 | var keyMap = map[Key]KeyInfo{}
21 |
22 | // keyMapShifted for shifted key description
23 | var keyMapShifted = map[Key]KeyInfo{}
24 |
25 | var keyShiftedMap = map[Key]Key{}
26 |
27 | // AddKey to KeyMap
28 | func AddKey(key string, shiftedKey string, code string, keyCode int, location int) Key {
29 | if len(key) == 1 {
30 | r := Key(key[0])
31 | if _, has := keyMap[r]; !has {
32 | keyMap[r] = KeyInfo{key, code, keyCode, location}
33 |
34 | if len(shiftedKey) == 1 {
35 | rs := Key(shiftedKey[0])
36 | keyMapShifted[rs] = KeyInfo{shiftedKey, code, keyCode, location}
37 | keyShiftedMap[r] = rs
38 | }
39 | return r
40 | }
41 | }
42 |
43 | k := Key(keyCode + (location+1)*256)
44 | keyMap[k] = KeyInfo{key, code, keyCode, location}
45 |
46 | return k
47 | }
48 |
49 | // Info of the key
50 | func (k Key) Info() KeyInfo {
51 | if k, has := keyMap[k]; has {
52 | return k
53 | }
54 | if k, has := keyMapShifted[k]; has {
55 | return k
56 | }
57 |
58 | panic("key not defined")
59 | }
60 |
61 | // KeyInfo of a key
62 | // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
63 | type KeyInfo struct {
64 | // Here's the value for Shift key on the keyboard
65 |
66 | Key string // Shift
67 | Code string // ShiftLeft
68 | KeyCode int // 16
69 | Location int // 1
70 | }
71 |
72 | // Shift returns the shifted key, such as shifted "1" is "!".
73 | func (k Key) Shift() (Key, bool) {
74 | s, has := keyShiftedMap[k]
75 | return s, has
76 | }
77 |
78 | // Printable returns true if the key is printable
79 | func (k Key) Printable() bool {
80 | return len(k.Info().Key) == 1
81 | }
82 |
83 | // Modifier returns the modifier value of the key
84 | func (k Key) Modifier() int {
85 | switch k.Info().KeyCode {
86 | case 18:
87 | return ModifierAlt
88 | case 17:
89 | return ModifierControl
90 | case 91, 92:
91 | return ModifierMeta
92 | case 16:
93 | return ModifierShift
94 | }
95 | return 0
96 | }
97 |
98 | // Encode general key event
99 | func (k Key) Encode(t proto.InputDispatchKeyEventType, modifiers int) *proto.InputDispatchKeyEvent {
100 | tp := t
101 | if t == proto.InputDispatchKeyEventTypeKeyDown && !k.Printable() {
102 | tp = proto.InputDispatchKeyEventTypeRawKeyDown
103 | }
104 |
105 | info := k.Info()
106 | l := gson.Int(info.Location)
107 | keypad := false
108 | if info.Location == 3 {
109 | l = nil
110 | keypad = true
111 | }
112 |
113 | txt := ""
114 | if k.Printable() {
115 | txt = info.Key
116 | }
117 |
118 | var cmd []string
119 | if IsMac {
120 | cmd = macCommands[info.Key]
121 | }
122 |
123 | e := &proto.InputDispatchKeyEvent{
124 | Type: tp,
125 | WindowsVirtualKeyCode: info.KeyCode,
126 | Code: info.Code,
127 | Key: info.Key,
128 | Text: txt,
129 | UnmodifiedText: txt,
130 | Location: l,
131 | IsKeypad: keypad,
132 | Modifiers: modifiers,
133 | Commands: cmd,
134 | }
135 |
136 | return e
137 | }
138 |
--------------------------------------------------------------------------------
/lib/input/keyboard_test.go:
--------------------------------------------------------------------------------
1 | package input_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/go-rod/rod/lib/input"
7 | "github.com/go-rod/rod/lib/proto"
8 | "github.com/ysmood/got"
9 | "github.com/ysmood/got/lib/gop"
10 | "github.com/ysmood/gson"
11 | )
12 |
13 | func TestKeyMap(t *testing.T) {
14 | g := got.T(t)
15 |
16 | k := input.Key('a')
17 | g.Eq(k.Info(), input.KeyInfo{
18 | Key: "a",
19 | Code: "KeyA",
20 | KeyCode: 65,
21 | Location: 0,
22 | })
23 |
24 | k = input.Key('A')
25 | g.Eq(k.Info(), input.KeyInfo{
26 | Key: "A",
27 | Code: "KeyA",
28 | KeyCode: 65,
29 | Location: 0,
30 | })
31 | g.True(k.Printable())
32 |
33 | k = input.Enter
34 | g.Eq(k.Info(), input.KeyInfo{
35 | Key: "\r",
36 | Code: "Enter",
37 | KeyCode: 13,
38 | Location: 0,
39 | })
40 |
41 | k = input.ShiftLeft
42 | g.Eq(k.Info(), input.KeyInfo /* len=4 */ {
43 | Key: "Shift",
44 | Code: "ShiftLeft",
45 | KeyCode: 16,
46 | Location: 1,
47 | })
48 | g.False(k.Printable())
49 |
50 | k = input.ShiftRight
51 | g.Eq(k.Info(), input.KeyInfo /* len=4 */ {
52 | Key: "Shift",
53 | Code: "ShiftRight",
54 | KeyCode: 16,
55 | Location: 2,
56 | })
57 |
58 | k, has := input.Digit1.Shift()
59 | g.True(has)
60 | g.Eq(k.Info().Key, "!")
61 |
62 | _, has = input.Enter.Shift()
63 | g.False(has)
64 |
65 | g.Panic(func() {
66 | input.Key('\n').Info()
67 | })
68 | }
69 |
70 | func TestKeyModifier(t *testing.T) {
71 | g := got.T(t)
72 |
73 | check := func(k input.Key, m int) {
74 | g.Helper()
75 |
76 | g.Eq(k.Modifier(), m)
77 | }
78 |
79 | check(input.KeyA, 0)
80 | check(input.AltLeft, 1)
81 | check(input.ControlLeft, 2)
82 | check(input.MetaLeft, 4)
83 | check(input.ShiftLeft, 8)
84 | }
85 |
86 | func TestKeyEncode(t *testing.T) {
87 | g := got.T(t)
88 |
89 | g.Eq(input.Key('a').Encode(proto.InputDispatchKeyEventTypeKeyDown, 0), &proto.InputDispatchKeyEvent{
90 | Type: "keyDown",
91 | Text: "a",
92 | UnmodifiedText: "a",
93 | Code: "KeyA",
94 | Key: "a",
95 | WindowsVirtualKeyCode: 65,
96 | Location: gson.Int(0),
97 | })
98 |
99 | g.Eq(input.Key('a').Encode(proto.InputDispatchKeyEventTypeKeyUp, 0), &proto.InputDispatchKeyEvent{
100 | Type: "keyUp",
101 | Text: "a",
102 | UnmodifiedText: "a",
103 | Code: "KeyA",
104 | Key: "a",
105 | WindowsVirtualKeyCode: 65,
106 | Location: gson.Int(0),
107 | })
108 |
109 | g.Eq(input.AltLeft.Encode(proto.InputDispatchKeyEventTypeKeyDown, 0), &proto.InputDispatchKeyEvent{
110 | Type: "rawKeyDown",
111 | Code: "AltLeft",
112 | Key: "Alt",
113 | WindowsVirtualKeyCode: 18,
114 | Location: gson.Int(1),
115 | })
116 |
117 | g.Eq(input.Numpad1.Encode(proto.InputDispatchKeyEventTypeKeyDown, 0), &proto.InputDispatchKeyEvent{
118 | Type: "keyDown",
119 | Code: "Numpad1",
120 | Key: "1",
121 | Text: "1",
122 | UnmodifiedText: "1",
123 | WindowsVirtualKeyCode: 35,
124 | IsKeypad: true,
125 | })
126 | }
127 |
128 | func TestMac(t *testing.T) {
129 | g := got.T(t)
130 |
131 | old := input.IsMac
132 | input.IsMac = true
133 | defer func() { input.IsMac = old }()
134 |
135 | g.Eq(input.ArrowDown.Encode(proto.InputDispatchKeyEventTypeKeyDown, 0), &proto.InputDispatchKeyEvent{
136 | Type: "rawKeyDown",
137 | Code: "ArrowDown",
138 | Key: "ArrowDown",
139 | WindowsVirtualKeyCode: 40,
140 | AutoRepeat: false,
141 | IsKeypad: false,
142 | IsSystemKey: false,
143 | Location: gop.Ptr(0).(*int),
144 | Commands: []string{
145 | "moveDown",
146 | },
147 | })
148 | }
149 |
--------------------------------------------------------------------------------
/lib/input/mouse.go:
--------------------------------------------------------------------------------
1 | package input
2 |
3 | import "github.com/go-rod/rod/lib/proto"
4 |
5 | // MouseKeys is the map for mouse keys
6 | var MouseKeys = map[proto.InputMouseButton]int{
7 | proto.InputMouseButtonLeft: 1,
8 | proto.InputMouseButtonRight: 2,
9 | proto.InputMouseButtonMiddle: 4,
10 | proto.InputMouseButtonBack: 8,
11 | proto.InputMouseButtonForward: 16,
12 | }
13 |
14 | // EncodeMouseButton into button flag
15 | func EncodeMouseButton(buttons []proto.InputMouseButton) (proto.InputMouseButton, int) {
16 | flag := int(0)
17 | for _, btn := range buttons {
18 | flag |= MouseKeys[btn]
19 | }
20 | btn := proto.InputMouseButton("none")
21 | if len(buttons) > 0 {
22 | btn = buttons[0]
23 | }
24 | return btn, flag
25 | }
26 |
--------------------------------------------------------------------------------
/lib/input/mouse_test.go:
--------------------------------------------------------------------------------
1 | package input_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/go-rod/rod/lib/input"
7 | "github.com/go-rod/rod/lib/proto"
8 | "github.com/ysmood/got"
9 | )
10 |
11 | func TestMouseEncode(t *testing.T) {
12 | g := got.T(t)
13 |
14 | b, flag := input.EncodeMouseButton([]proto.InputMouseButton{proto.InputMouseButtonLeft})
15 |
16 | g.Eq(b, proto.InputMouseButtonLeft)
17 | g.Eq(flag, 1)
18 | }
19 |
--------------------------------------------------------------------------------
/lib/js/generate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strings"
7 |
8 | "github.com/go-rod/rod/lib/utils"
9 | "github.com/ysmood/gson"
10 | )
11 |
12 | func main() {
13 | list := getList()
14 | out := `// generated by "lib/js/generate"` +
15 | "\n\npackage js\n\n"
16 |
17 | for _, fn := range list.Arr() {
18 | name := fn.Get("name").Str()
19 | def := fn.Get("definition").Str()
20 | out += utils.S(`
21 | // {{.Name}} ...
22 | var {{.Name}} = &Function{
23 | Name: "{{.name}}",
24 | Definition: {{.definition}},
25 | Dependencies: {{.dependencies}},
26 | }
27 | `,
28 | "Name", fnName(name),
29 | "name", name,
30 | "definition", utils.EscapeGoString(def),
31 | "dependencies", getDeps(def),
32 | )
33 | }
34 |
35 | utils.E(utils.OutputFile("lib/js/helper.go", out))
36 |
37 | utils.Exec("gofmt -s -w lib/js/helper.go")
38 | }
39 |
40 | var regDeps = regexp.MustCompile(`\Wfunctions.(\w+)`)
41 |
42 | func getDeps(fn string) string {
43 | ms := regDeps.FindAllStringSubmatch(fn, -1)
44 |
45 | list := []string{}
46 |
47 | for _, m := range ms {
48 | list = append(list, fnName(m[1]))
49 | }
50 |
51 | return "[]*Function{" + strings.Join(list, ",") + "}"
52 | }
53 |
54 | func fnName(name string) string {
55 | return strings.ToUpper(name[0:1]) + name[1:]
56 | }
57 |
58 | func getList() gson.JSON {
59 | code := utils.ExecLine(false, "npx -ys -- uglify-js@3.14.5 -c -m -- lib/js/helper.js")
60 |
61 | script := fmt.Sprintf(`
62 | %s
63 |
64 | const list = []
65 |
66 | for (const name in functions) {
67 | const reg = new RegExp('^(async )?' + name)
68 | const definition = functions[name].toString().replace(reg, '$1function')
69 | list.push({name, definition})
70 | }
71 |
72 | console.log(JSON.stringify(list))
73 | `, string(code))
74 |
75 | tmp := "tmp/helper.js"
76 |
77 | utils.E(utils.OutputFile(tmp, script))
78 |
79 | return gson.NewFrom(utils.ExecLine(false, "node", tmp))
80 | }
81 |
--------------------------------------------------------------------------------
/lib/js/js.go:
--------------------------------------------------------------------------------
1 | package js
2 |
3 | // Function definition
4 | type Function struct {
5 | // Name must be unique and not conflict with the function names in "helper.js"
6 | Name string
7 |
8 | // Definition holds the code of a js function from "helper.js",
9 | // the js code is compressed by uglify-js.
10 | Definition string
11 |
12 | // Dependencies will be preloaded and assigned to the global js object "functions"
13 | Dependencies []*Function
14 | }
15 |
16 | // Functions ...
17 | var Functions = &Function{
18 | Name: "functions",
19 | Definition: "() => ({})",
20 | Dependencies: nil,
21 | }
22 |
--------------------------------------------------------------------------------
/lib/launcher/README.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | A lib helps to find, launch or download the browser. You can also use it as a standalone lib without Rod.
4 |
--------------------------------------------------------------------------------
/lib/launcher/example_test.go:
--------------------------------------------------------------------------------
1 | package launcher_test
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 |
7 | "github.com/go-rod/rod"
8 | "github.com/go-rod/rod/lib/launcher"
9 | "github.com/go-rod/rod/lib/utils"
10 | "github.com/ysmood/leakless"
11 | )
12 |
13 | func Example_use_system_browser() {
14 | if path, exists := launcher.LookPath(); exists {
15 | u := launcher.New().Bin(path).MustLaunch()
16 | rod.New().ControlURL(u).MustConnect()
17 | }
18 | }
19 |
20 | func Example_print_browser_CLI_output() {
21 | // Pipe the browser stderr and stdout to os.Stdout .
22 | u := launcher.New().Logger(os.Stdout).MustLaunch()
23 | rod.New().ControlURL(u).MustConnect()
24 | }
25 |
26 | func Example_custom_launch() {
27 | // get the browser executable path
28 | path := launcher.NewBrowser().MustGet()
29 |
30 | // use the FormatArgs to construct args, this line is optional, you can construct the args manually
31 | args := launcher.New().FormatArgs()
32 |
33 | var cmd *exec.Cmd
34 | if true { // decide whether to use leakless or not
35 | cmd = leakless.New().Command(path, args...)
36 | } else {
37 | cmd = exec.Command(path, args...)
38 | }
39 |
40 | parser := launcher.NewURLParser()
41 | cmd.Stderr = parser
42 | utils.E(cmd.Start())
43 | u := launcher.MustResolveURL(<-parser.URL)
44 |
45 | rod.New().ControlURL(u).MustConnect()
46 | }
47 |
--------------------------------------------------------------------------------
/lib/launcher/flags/flags.go:
--------------------------------------------------------------------------------
1 | package flags
2 |
3 | // Flag name of a command line argument of the browser, also known as command line flag or switch.
4 | // List of available flags: https://peter.sh/experiments/chromium-command-line-switches
5 | type Flag string
6 |
7 | // TODO: we should automatically generate all the flags here
8 | const (
9 | // UserDataDir flag
10 | UserDataDir Flag = "user-data-dir"
11 |
12 | // Headless mode. Whether to run browser in headless mode. A mode without visible UI.
13 | Headless Flag = "headless"
14 |
15 | // App flag
16 | App Flag = "app"
17 |
18 | // RemoteDebuggingPort flag
19 | RemoteDebuggingPort Flag = "remote-debugging-port"
20 |
21 | // NoSandbox flag
22 | NoSandbox Flag = "no-sandbox"
23 |
24 | // ProxyServer flag
25 | ProxyServer Flag = "proxy-server"
26 |
27 | // WorkingDir flag
28 | WorkingDir Flag = "rod-working-dir"
29 |
30 | // Env flag
31 | Env Flag = "rod-env"
32 |
33 | // XVFB flag
34 | XVFB Flag = "rod-xvfb"
35 |
36 | // Leakless flag
37 | Leakless Flag = "rod-leakless"
38 |
39 | // Bin is the browser executable file path. If it's empty, launcher will automatically search or download the bin.
40 | Bin Flag = "rod-bin"
41 |
42 | // KeepUserDataDir flag
43 | KeepUserDataDir Flag = "rod-keep-user-data-dir"
44 |
45 | // Arguments for the command. Such as
46 | // chrome-bin http://a.com http://b.com
47 | // The "http://a.com" and "http://b.com" are the arguments
48 | Arguments Flag = ""
49 | )
50 |
--------------------------------------------------------------------------------
/lib/launcher/load_test.go:
--------------------------------------------------------------------------------
1 | package launcher_test
2 |
3 | import (
4 | "context"
5 | "math/rand"
6 | "sync"
7 | "testing"
8 |
9 | "github.com/go-rod/rod"
10 | "github.com/go-rod/rod/lib/cdp"
11 | "github.com/go-rod/rod/lib/launcher"
12 | "github.com/go-rod/rod/lib/utils"
13 | "github.com/ysmood/got"
14 | )
15 |
16 | func BenchmarkManager(b *testing.B) {
17 | const concurrent = 30 // how many browsers will run at the same time
18 | const num = 300 // how many browsers we will launch
19 |
20 | limiter := make(chan int, concurrent)
21 |
22 | s := got.New(b).Serve()
23 |
24 | // docker run --rm -p 7317:7317 ghcr.io/go-rod/rod
25 | s.HostURL.Host = "host.docker.internal"
26 |
27 | s.Route("/", ".html", `
28 | ok
29 | `)
34 |
35 | wg := &sync.WaitGroup{}
36 | wg.Add(num)
37 | for i := 0; i < num; i++ {
38 | limiter <- 0
39 |
40 | go func() {
41 | utils.Sleep(rand.Float64())
42 |
43 | ctx, cancel := context.WithCancel(context.Background())
44 | defer func() {
45 | go func() {
46 | utils.Sleep(2)
47 | cancel()
48 | }()
49 | }()
50 |
51 | l := launcher.MustNewManaged("")
52 | u, h := l.ClientHeader()
53 | browser := rod.New().Client(cdp.MustStartWithURL(ctx, u, h)).MustConnect()
54 | page := browser.MustPage()
55 | wait := page.MustWaitNavigation()
56 | page.MustNavigate(s.URL())
57 | wait()
58 | page.MustEval(`wait()`)
59 |
60 | if rand.Int()%10 == 0 {
61 | // 10% we will drop the websocket connection without call the api to gracefully close the browser
62 | cancel()
63 | } else {
64 | browser.MustClose()
65 | }
66 |
67 | wg.Done()
68 | <-limiter
69 | }()
70 | }
71 | wg.Wait()
72 | }
73 |
--------------------------------------------------------------------------------
/lib/launcher/os_unix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package launcher
4 |
5 | import (
6 | "os/exec"
7 | "syscall"
8 |
9 | "github.com/go-rod/rod/lib/launcher/flags"
10 | )
11 |
12 | func killGroup(pid int) {
13 | _ = syscall.Kill(-pid, syscall.SIGKILL)
14 | }
15 |
16 | func (l *Launcher) osSetupCmd(cmd *exec.Cmd) {
17 | if flags, has := l.GetFlags(flags.XVFB); has {
18 | var command []string
19 | // flags must append before cmd.Args
20 | command = append(command, flags...)
21 | command = append(command, cmd.Args...)
22 |
23 | *cmd = *exec.Command("xvfb-run", command...)
24 | }
25 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
26 | }
27 |
--------------------------------------------------------------------------------
/lib/launcher/os_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package launcher
4 |
5 | import (
6 | "os/exec"
7 | "syscall"
8 | )
9 |
10 | func killGroup(pid int) {
11 | terminateProcess(pid)
12 | }
13 |
14 | func (l *Launcher) osSetupCmd(cmd *exec.Cmd) {
15 | cmd.SysProcAttr = &syscall.SysProcAttr{
16 | CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
17 | }
18 | }
19 |
20 | func terminateProcess(pid int) {
21 | handle, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, true, uint32(pid))
22 | if err != nil {
23 | return
24 | }
25 |
26 | syscall.TerminateProcess(handle, 0)
27 | syscall.CloseHandle(handle)
28 | }
29 |
--------------------------------------------------------------------------------
/lib/launcher/revision.go:
--------------------------------------------------------------------------------
1 | // generated by "lib/launcher/revision"
2 |
3 | package launcher
4 |
5 | // RevisionDefault for chromium
6 | const RevisionDefault = 1030087
7 |
8 | // RevisionPlaywright for arm linux
9 | const RevisionPlaywright = 1015
10 |
--------------------------------------------------------------------------------
/lib/launcher/revision/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "net/http"
9 | "path/filepath"
10 | "sort"
11 | "strconv"
12 | "strings"
13 |
14 | "github.com/go-rod/rod/lib/utils"
15 | "github.com/ysmood/gson"
16 | )
17 |
18 | const mirror = "https://registry.npmmirror.com/-/binary/chromium-browser-snapshots/"
19 |
20 | func main() {
21 | list := getList(mirror)
22 |
23 | revLists := [][]int{}
24 | for _, os := range list {
25 | revList := []int{}
26 | for _, s := range getList(mirror + os + "/") {
27 | rev, err := strconv.ParseInt(s, 10, 32)
28 | if err != nil {
29 | log.Fatal(err)
30 | }
31 | revList = append(revList, int(rev))
32 | }
33 | sort.Ints(revList)
34 | revLists = append(revLists, revList)
35 | }
36 |
37 | rev := largestCommonRevision(revLists)
38 |
39 | if rev < 969819 {
40 | utils.E(fmt.Errorf("cannot match version of the latest chromium from %s", mirror))
41 | }
42 |
43 | playwright := getFromPlaywright()
44 |
45 | out := utils.S(`// generated by "lib/launcher/revision"
46 |
47 | package launcher
48 |
49 | // RevisionDefault for chromium
50 | const RevisionDefault = {{.default}}
51 |
52 | // RevisionPlaywright for arm linux
53 | const RevisionPlaywright = {{.playwright}}
54 | `,
55 | "default", rev,
56 | "playwright", playwright,
57 | )
58 |
59 | utils.E(utils.OutputFile(filepath.FromSlash("lib/launcher/revision.go"), out))
60 |
61 | }
62 |
63 | func getList(path string) []string {
64 | res, err := http.Get(path)
65 | utils.E(err)
66 | defer func() { _ = res.Body.Close() }()
67 |
68 | var data interface{}
69 | err = json.NewDecoder(res.Body).Decode(&data)
70 | utils.E(err)
71 |
72 | list := data.([]interface{})
73 |
74 | names := []string{}
75 | for _, it := range list {
76 | name := it.(map[string]interface{})["name"].(string)
77 | names = append(names, strings.TrimRight(name, "/"))
78 | }
79 |
80 | return names
81 | }
82 |
83 | func largestCommonRevision(revLists [][]int) int {
84 | sort.Slice(revLists, func(i, j int) bool {
85 | return len(revLists[i]) < len(revLists[j])
86 | })
87 |
88 | shortest := revLists[0]
89 |
90 | for i := len(shortest) - 1; i >= 0; i-- {
91 | r := shortest[i]
92 |
93 | isCommon := true
94 | for i := 1; i < len(revLists); i++ {
95 | if !has(revLists[i], r) {
96 | isCommon = false
97 | break
98 | }
99 | }
100 | if isCommon {
101 | return r
102 | }
103 | }
104 |
105 | return 0
106 | }
107 |
108 | func has(list []int, i int) bool {
109 | index := sort.SearchInts(list, i)
110 | return index < len(list) && list[index] == i
111 | }
112 |
113 | func getFromPlaywright() int {
114 | pv := strings.TrimSpace(utils.ExecLine(false, "npm --no-update-notifier -s show playwright version"))
115 | out := fetch(fmt.Sprintf("https://raw.githubusercontent.com/microsoft/playwright/v%s/packages/playwright-core/browsers.json", pv))
116 | rev, err := strconv.ParseInt(gson.NewFrom(out).Get("browsers.0.revision").Str(), 10, 32)
117 | utils.E(err)
118 | return int(rev)
119 | }
120 |
121 | func fetch(u string) string {
122 | res, err := http.Get(u)
123 | utils.E(err)
124 | defer func() { _ = res.Body.Close() }()
125 |
126 | b, err := ioutil.ReadAll(res.Body)
127 | utils.E(err)
128 | return string(b)
129 | }
130 |
--------------------------------------------------------------------------------
/lib/launcher/rod-manager/main.go:
--------------------------------------------------------------------------------
1 | // A server to help launch browser remotely
2 | package main
3 |
4 | import (
5 | "flag"
6 | "fmt"
7 | "log"
8 | "net"
9 | "net/http"
10 | "os"
11 |
12 | "github.com/go-rod/rod/lib/launcher"
13 | "github.com/go-rod/rod/lib/utils"
14 | )
15 |
16 | var addr = flag.String("address", ":7317", "the address to listen to")
17 | var quiet = flag.Bool("quiet", false, "silence the log")
18 | var allowAllPath = flag.Bool("allow-all", false, "allow all path set by the client")
19 |
20 | func main() {
21 | flag.Parse()
22 |
23 | m := launcher.NewManager()
24 |
25 | if !*quiet {
26 | m.Logger = log.New(os.Stdout, "", 0)
27 | }
28 |
29 | if *allowAllPath {
30 | m.BeforeLaunch = func(l *launcher.Launcher, rw http.ResponseWriter, r *http.Request) {}
31 | }
32 |
33 | l, err := net.Listen("tcp", *addr)
34 | if err != nil {
35 | utils.E(err)
36 | }
37 |
38 | if !*quiet {
39 | fmt.Println("rod-manager listening on:", l.Addr().String())
40 | }
41 |
42 | srv := &http.Server{Handler: m}
43 | utils.E(srv.Serve(l))
44 | }
45 |
--------------------------------------------------------------------------------
/lib/launcher/url_parser.go:
--------------------------------------------------------------------------------
1 | package launcher
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "io"
7 | "io/ioutil"
8 | "net/http"
9 | "net/url"
10 | "regexp"
11 | "strings"
12 | "sync"
13 |
14 | "github.com/go-rod/rod/lib/utils"
15 | "github.com/ysmood/gson"
16 | )
17 |
18 | var _ io.Writer = &URLParser{}
19 |
20 | // URLParser to get control url from stderr
21 | type URLParser struct {
22 | URL chan string
23 | Buffer string // buffer for the browser stdout
24 |
25 | lock *sync.Mutex
26 | ctx context.Context
27 | done bool
28 | }
29 |
30 | // NewURLParser instance
31 | func NewURLParser() *URLParser {
32 | return &URLParser{
33 | URL: make(chan string),
34 | lock: &sync.Mutex{},
35 | ctx: context.Background(),
36 | }
37 | }
38 |
39 | var regWS = regexp.MustCompile(`ws://.+/`)
40 |
41 | // Context sets the context
42 | func (r *URLParser) Context(ctx context.Context) *URLParser {
43 | r.ctx = ctx
44 | return r
45 | }
46 |
47 | // Write interface
48 | func (r *URLParser) Write(p []byte) (n int, err error) {
49 | r.lock.Lock()
50 | defer r.lock.Unlock()
51 |
52 | if !r.done {
53 | r.Buffer += string(p)
54 |
55 | str := regWS.FindString(r.Buffer)
56 | if str != "" {
57 | u, err := url.Parse(strings.TrimSpace(str))
58 | utils.E(err)
59 |
60 | select {
61 | case <-r.ctx.Done():
62 | case r.URL <- "http://" + u.Host:
63 | }
64 |
65 | r.done = true
66 | r.Buffer = ""
67 | }
68 | }
69 |
70 | return len(p), nil
71 | }
72 |
73 | // Err returns the common error parsed from stdout and stderr
74 | func (r *URLParser) Err() error {
75 | r.lock.Lock()
76 | defer r.lock.Unlock()
77 |
78 | msg := "[launcher] Failed to get the debug url: "
79 |
80 | if strings.Contains(r.Buffer, "error while loading shared libraries") {
81 | msg = "[launcher] Failed to launch the browser, the doc might help https://go-rod.github.io/#/compatibility?id=os: "
82 | }
83 |
84 | return errors.New(msg + r.Buffer)
85 | }
86 |
87 | // MustResolveURL is similar to ResolveURL
88 | func MustResolveURL(u string) string {
89 | u, err := ResolveURL(u)
90 | utils.E(err)
91 | return u
92 | }
93 |
94 | var regPort = regexp.MustCompile(`^\:?(\d+)$`)
95 | var regProtocol = regexp.MustCompile(`^\w+://`)
96 |
97 | // ResolveURL by requesting the u, it will try best to normalize the u.
98 | // The format of u can be "9222", ":9222", "host:9222", "ws://host:9222", "wss://host:9222",
99 | // "https://host:9222" "http://host:9222". The return string will look like:
100 | // "ws://host:9222/devtools/browser/4371405f-84df-4ad6-9e0f-eab81f7521cc"
101 | func ResolveURL(u string) (string, error) {
102 | if u == "" {
103 | u = "9222"
104 | }
105 |
106 | u = strings.TrimSpace(u)
107 | u = regPort.ReplaceAllString(u, "127.0.0.1:$1")
108 |
109 | if !regProtocol.MatchString(u) {
110 | u = "http://" + u
111 | }
112 |
113 | parsed, err := url.Parse(u)
114 | if err != nil {
115 | return "", err
116 | }
117 |
118 | parsed = toHTTP(*parsed)
119 | parsed.Path = "/json/version"
120 |
121 | res, err := http.Get(parsed.String())
122 | if err != nil {
123 | return "", err
124 | }
125 | defer func() { _ = res.Body.Close() }()
126 |
127 | data, err := ioutil.ReadAll(res.Body)
128 | utils.E(err)
129 |
130 | return gson.New(data).Get("webSocketDebuggerUrl").Str(), nil
131 | }
132 |
--------------------------------------------------------------------------------
/lib/launcher/utils.go:
--------------------------------------------------------------------------------
1 | package launcher
2 |
3 | import (
4 | "archive/zip"
5 | "fmt"
6 | "io"
7 | "net/url"
8 | "os"
9 | "path/filepath"
10 | "time"
11 |
12 | "github.com/go-rod/rod/lib/utils"
13 | )
14 |
15 | var inContainer = utils.InContainer
16 |
17 | type progresser struct {
18 | size int
19 | count int
20 | logger utils.Logger
21 | last time.Time
22 | }
23 |
24 | func (p *progresser) Write(b []byte) (n int, err error) {
25 | n = len(b)
26 |
27 | if p.count == 0 {
28 | p.logger.Println("Progress:")
29 | }
30 |
31 | p.count += n
32 |
33 | if p.count == p.size {
34 | p.logger.Println("100%")
35 | return
36 | }
37 |
38 | if time.Since(p.last) < time.Second {
39 | return
40 | }
41 |
42 | p.last = time.Now()
43 | p.logger.Println(fmt.Sprintf("%02d%%", p.count*100/p.size))
44 |
45 | return
46 | }
47 |
48 | func toHTTP(u url.URL) *url.URL {
49 | newURL := u
50 | if newURL.Scheme == "ws" {
51 | newURL.Scheme = "http"
52 | } else if newURL.Scheme == "wss" {
53 | newURL.Scheme = "https"
54 | }
55 | return &newURL
56 | }
57 |
58 | func toWS(u url.URL) *url.URL {
59 | newURL := u
60 | if newURL.Scheme == "http" {
61 | newURL.Scheme = "ws"
62 | } else if newURL.Scheme == "https" {
63 | newURL.Scheme = "wss"
64 | }
65 | return &newURL
66 | }
67 |
68 | func unzip(logger utils.Logger, from, to string) (err error) {
69 | defer func() {
70 | if e := recover(); e != nil {
71 | err = e.(error)
72 | }
73 | }()
74 |
75 | logger.Println("Unzip to:", to)
76 |
77 | zr, err := zip.OpenReader(from)
78 | utils.E(err)
79 |
80 | size := 0
81 | for _, f := range zr.File {
82 | size += int(f.FileInfo().Size())
83 | }
84 |
85 | progress := &progresser{size: size, logger: logger}
86 |
87 | for _, f := range zr.File {
88 | p := filepath.Join(to, f.Name)
89 |
90 | _ = utils.Mkdir(filepath.Dir(p))
91 |
92 | if f.FileInfo().IsDir() {
93 | err := os.Mkdir(p, f.Mode())
94 | utils.E(err)
95 | continue
96 | }
97 |
98 | r, err := f.Open()
99 | utils.E(err)
100 |
101 | dst, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, f.Mode())
102 | utils.E(err)
103 |
104 | _, err = io.Copy(io.MultiWriter(dst, progress), r)
105 | utils.E(err)
106 |
107 | err = dst.Close()
108 | utils.E(err)
109 | }
110 |
111 | return zr.Close()
112 | }
113 |
--------------------------------------------------------------------------------
/lib/proto/README.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | A lib to encode/decode the data of the cdp protocol.
4 |
5 | This lib is standalone and stateless, you can use it independently. Such as use it to encode/decode JSON with other libs that can drive browsers.
6 |
7 | Here's an [usage example](https://github.com/go-rod/rod/blob/9e847f3bab313a1d233c0c868fe5125e2e70de70/examples_test.go#L370-L393).
8 |
--------------------------------------------------------------------------------
/lib/proto/a_interface.go:
--------------------------------------------------------------------------------
1 | // Package proto is a lib to encode/decode the data of the cdp protocol.
2 | // Package proto是对cdp协议的数据进行编码/解码的库。
3 | package proto
4 |
5 | import (
6 | "context"
7 | "encoding/json"
8 | "reflect"
9 | "strings"
10 | )
11 |
12 | // Client interface to send the request.
13 | // 用于发送请求的客户端接口
14 | // So that this lib doesn't handle anything has side effect.
15 | // 所以这个库不会处理任何有副作用的东西。
16 | type Client interface {
17 | Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error)
18 | }
19 |
20 | // Sessionable type has a proto.TargetSessionID for its methods
21 | // Sessionable 有一个 proto.TargetSessionID 方法
22 | type Sessionable interface {
23 | GetSessionID() TargetSessionID
24 | }
25 |
26 | // Contextable type has a context.Context for its methods
27 | // Contextable 有一个 context.Context 方法
28 | type Contextable interface {
29 | GetContext() context.Context
30 | }
31 |
32 | // Request represents a cdp.Request.Method
33 | // 代表一个cdp.Request.Method
34 | type Request interface {
35 | // ProtoReq returns the cdp.Request.Method
36 | // 返回 cdp.Request.Method
37 | ProtoReq() string
38 | }
39 |
40 | // Event represents a cdp.Event.Params
41 | // Event 代表 cdp.Event.Params
42 | type Event interface {
43 | // ProtoEvent returns the cdp.Event.Method
44 | ProtoEvent() string
45 | }
46 |
47 | // GetType from method name of this package,
48 | // such as proto.GetType("Page.enable") will return the type of proto.PageEnable
49 | // 从这个包的方法名中获取类型,例如proto.GetType("Page.enable")将返回proto.PageEnable的类型。
50 | func GetType(methodName string) reflect.Type {
51 | return types[methodName]
52 | }
53 |
54 | // ParseMethodName to domain and name
55 | // 解析方法的 domain 和 name
56 | func ParseMethodName(method string) (domain, name string) {
57 | arr := strings.Split(method, ".")
58 | return arr[0], arr[1]
59 | }
60 |
61 | // call method with request and response containers.
62 | // 具有请求和响应容器的调用方式。
63 | func call(method string, req, res interface{}, c Client) error {
64 | ctx := context.Background()
65 | if cta, ok := c.(Contextable); ok {
66 | ctx = cta.GetContext()
67 | }
68 |
69 | sessionID := ""
70 | if tsa, ok := c.(Sessionable); ok {
71 | sessionID = string(tsa.GetSessionID())
72 | }
73 |
74 | bin, err := c.Call(ctx, sessionID, method, req)
75 | if err != nil {
76 | return err
77 | }
78 | if res == nil {
79 | return nil
80 | }
81 | return json.Unmarshal(bin, res)
82 | }
83 |
--------------------------------------------------------------------------------
/lib/proto/a_interface_test.go:
--------------------------------------------------------------------------------
1 | package proto_test
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "reflect"
8 |
9 | "github.com/go-rod/rod/lib/proto"
10 | "github.com/go-rod/rod/lib/utils"
11 | )
12 |
13 | type Client struct {
14 | sessionID string
15 | methodName string
16 | params interface{}
17 | err error
18 | ret interface{}
19 | }
20 |
21 | var _ proto.Client = &Client{}
22 | var _ proto.Sessionable = &Client{}
23 | var _ proto.Contextable = &Client{}
24 |
25 | func (c *Client) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) {
26 | c.sessionID = sessionID
27 | c.methodName = methodName
28 | c.params = params
29 | return utils.MustToJSONBytes(c.ret), c.err
30 | }
31 |
32 | func (c *Client) GetSessionID() proto.TargetSessionID { return "" }
33 |
34 | func (c *Client) GetContext() context.Context { return nil }
35 |
36 | func (t T) CallErr() {
37 | client := &Client{err: errors.New("err")}
38 | t.Eq(proto.PageEnable{}.Call(client).Error(), "err")
39 | }
40 |
41 | func (t T) ParseMethodName() {
42 | d, n := proto.ParseMethodName("Page.enable")
43 | t.Eq("Page", d)
44 | t.Eq("enable", n)
45 | }
46 |
47 | func (t T) GetType() {
48 | method := proto.GetType("Page.enable")
49 | t.Eq(reflect.TypeOf(proto.PageEnable{}), method)
50 | }
51 |
52 | func (t T) TimeCodec() {
53 | raw := []byte("123.123")
54 | var duration proto.MonotonicTime
55 | t.E(json.Unmarshal(raw, &duration))
56 |
57 | t.Eq(123123, duration.Duration().Milliseconds())
58 | t.Eq("2m3.123s", duration.String())
59 |
60 | data, err := json.Marshal(duration)
61 | t.E(err)
62 | t.Eq(raw, data)
63 |
64 | raw = []byte("1234567890")
65 | var datetime proto.TimeSinceEpoch
66 | t.E(json.Unmarshal(raw, &datetime))
67 |
68 | t.Eq(1234567890, datetime.Time().Unix())
69 | t.Has(datetime.String(), "2009-02")
70 |
71 | data, err = json.Marshal(datetime)
72 | t.E(err)
73 | t.Eq(raw, data)
74 | }
75 |
76 | func (t T) Rect() {
77 | rect := proto.DOMQuad{
78 | 336, 382, 361, 382, 361, 421, 336, 412,
79 | }
80 |
81 | t.Eq(348.5, rect.Center().X)
82 | t.Eq(399.25, rect.Center().Y)
83 |
84 | res := &proto.DOMGetContentQuadsResult{}
85 | t.Nil(res.OnePointInside())
86 |
87 | res = &proto.DOMGetContentQuadsResult{Quads: []proto.DOMQuad{{1, 1, 2, 1, 2, 1, 1, 1}}}
88 | t.Nil(res.OnePointInside())
89 |
90 | res = &proto.DOMGetContentQuadsResult{Quads: []proto.DOMQuad{rect}}
91 | pt := res.OnePointInside()
92 | t.Eq(348.5, pt.X)
93 | t.Eq(399.25, pt.Y)
94 |
95 | }
96 |
97 | func (t T) Area() {
98 | t.Eq(proto.DOMQuad{1, 1, 2, 1, 2, 1, 1, 1}.Area(), 0)
99 | t.Eq(proto.DOMQuad{1, 1, 2, 1, 2, 2, 1, 2}.Area(), 1)
100 | t.Eq(proto.DOMQuad{1, 1, 2, 1, 2, 4, 1, 3}.Area(), 2.5)
101 | }
102 |
103 | func (t T) Box() {
104 | res := &proto.DOMGetContentQuadsResult{Quads: []proto.DOMQuad{
105 | {1, 1, 2, 1, 2, 2, 1, 2},
106 | {2, 0, 3, 0, 3, 1, 2, 1},
107 | {0, 2, 1, 2, 1, 3, 0, 3},
108 | }}
109 | t.Eq(res.Box(), &proto.DOMRect{
110 | X: 0,
111 | Y: 0,
112 | Width: 3,
113 | Height: 3,
114 | })
115 |
116 | t.Nil((&proto.DOMGetContentQuadsResult{}).Box())
117 | }
118 |
119 | func (t T) InputTouchPointMoveTo() {
120 | p := &proto.InputTouchPoint{}
121 | p.MoveTo(1, 2)
122 |
123 | t.Eq(1, p.X)
124 | t.Eq(2, p.Y)
125 | }
126 |
127 | func (t T) CookiesToParams() {
128 | list := proto.CookiesToParams([]*proto.NetworkCookie{{
129 | Name: "name",
130 | Value: "val",
131 | }})
132 |
133 | t.Eq(list[0].Name, "name")
134 | t.Eq(list[0].Value, "val")
135 | }
136 |
137 | func (t T) GeneratorOptimize() {
138 | var _ proto.TargetTargetInfoType = proto.TargetTargetInfoTypeBackgroundPage
139 | var _ proto.TargetTargetInfoType = proto.TargetTargetInfoTypePage
140 |
141 | var _ proto.PageLifecycleEventName = proto.PageLifecycleEventNameInit
142 | var _ proto.PageLifecycleEventName = proto.PageLifecycleEventNameFirstContentfulPaint
143 | var _ proto.PageLifecycleEventName = proto.PageLifecycleEventNameFirstImagePaint
144 |
145 | a := proto.InputDispatchKeyEvent{}
146 | var _ proto.TimeSinceEpoch = a.Timestamp
147 | b := proto.NetworkCookie{}
148 | var _ proto.TimeSinceEpoch = b.Expires
149 |
150 | c := proto.NetworkDataReceived{}
151 | var _ proto.MonotonicTime = c.Timestamp
152 |
153 | d := proto.NetworkCookie{}
154 | var _ proto.TimeSinceEpoch = d.Expires
155 | }
156 |
--------------------------------------------------------------------------------
/lib/proto/a_patch.go:
--------------------------------------------------------------------------------
1 | // Patches to normalize the proto types
2 | // 用于规范化原型的补丁
3 |
4 | package proto
5 |
6 | import (
7 | "time"
8 | )
9 |
10 | // TimeSinceEpoch UTC time in seconds, counted from January 1, 1970.
11 | // TimeSinceEpoch UTC时间,以秒为单位,从1970年1月1日算起。
12 | // To convert a time.Time to TimeSinceEpoch, for example:
13 | // 转换时间。例如:
14 | // proto.TimeSinceEpoch(time.Now().Unix())
15 | // For session cookie, the value should be -1.
16 | // 对于会话cookie,该值应该是-1。
17 | type TimeSinceEpoch float64
18 |
19 | // Time interface
20 | func (t TimeSinceEpoch) Time() time.Time {
21 | return (time.Unix(0, 0)).Add(
22 | time.Duration(t * TimeSinceEpoch(time.Second)),
23 | )
24 | }
25 |
26 | // String interface
27 | func (t TimeSinceEpoch) String() string {
28 | return t.Time().String()
29 | }
30 |
31 | // MonotonicTime Monotonically increasing time in seconds since an arbitrary point in the past.
32 | // 单调时间(MonotonicTime) 从过去的一个任意点开始,以秒为单位单调地增加时间。
33 | type MonotonicTime float64
34 |
35 | // Duration interface
36 | func (t MonotonicTime) Duration() time.Duration {
37 | return time.Duration(t * MonotonicTime(time.Second))
38 | }
39 |
40 | // String interface
41 | func (t MonotonicTime) String() string {
42 | return t.Duration().String()
43 | }
44 |
45 | // Point from the origin (0, 0)
46 | // 从原点(0,0)开始的点
47 | type Point struct {
48 | X float64 `json:"x"`
49 | Y float64 `json:"y"`
50 | }
51 |
52 | // Len is the number of vertices
53 | // Len是顶点的数量
54 | func (q DOMQuad) Len() int {
55 | return len(q) / 2
56 | }
57 |
58 | // Each point
59 | // 返回每一个点
60 | func (q DOMQuad) Each(fn func(pt Point, i int)) {
61 | for i := 0; i < q.Len(); i++ {
62 | fn(Point{q[i*2], q[i*2+1]}, i)
63 | }
64 | }
65 |
66 | // Center of the polygon
67 | // 多边形的中心
68 | func (q DOMQuad) Center() Point {
69 | var x, y float64
70 | q.Each(func(pt Point, _ int) {
71 | x += pt.X
72 | y += pt.Y
73 | })
74 | return Point{x / float64(q.Len()), y / float64(q.Len())}
75 | }
76 |
77 | // Area of the polygon
78 | // 多边形的面积
79 | // https://en.wikipedia.org/wiki/Polygon#Area
80 | func (q DOMQuad) Area() float64 {
81 | area := 0.0
82 | l := len(q)/2 - 1
83 |
84 | for i := 0; i < l; i++ {
85 | area += q[i*2]*q[i*2+3] - q[i*2+2]*q[i*2+1]
86 | }
87 | area += q[l*2]*q[1] - q[0]*q[l*2+1]
88 |
89 | return area / 2
90 | }
91 |
92 | // OnePointInside the shape
93 | // 形状内部的一个点
94 | func (res *DOMGetContentQuadsResult) OnePointInside() *Point {
95 | for _, q := range res.Quads {
96 | if q.Area() >= 1 {
97 | pt := q.Center()
98 | return &pt
99 | }
100 | }
101 |
102 | return nil
103 | }
104 |
105 | // Box returns the smallest leveled rectangle that can cover the whole shape.
106 | // 返回可以覆盖整个形状的最小水平矩形。
107 | func (res *DOMGetContentQuadsResult) Box() (box *DOMRect) {
108 | return Shape(res.Quads).Box()
109 | }
110 |
111 | // Shape is a list of DOMQuad
112 | // Shape是DOMQuad的列表
113 | type Shape []DOMQuad
114 |
115 | // Box returns the smallest leveled rectangle that can cover the whole shape.
116 | // 返回可以覆盖整个形状的最小水平矩形。
117 | func (qs Shape) Box() (box *DOMRect) {
118 | if len(qs) == 0 {
119 | return
120 | }
121 |
122 | left := qs[0][0]
123 | top := qs[0][1]
124 | right := left
125 | bottom := top
126 |
127 | for _, q := range qs {
128 | q.Each(func(pt Point, _ int) {
129 | if pt.X < left {
130 | left = pt.X
131 | }
132 | if pt.Y < top {
133 | top = pt.Y
134 | }
135 | if pt.X > right {
136 | right = pt.X
137 | }
138 | if pt.Y > bottom {
139 | bottom = pt.Y
140 | }
141 | })
142 | }
143 |
144 | box = &DOMRect{left, top, right - left, bottom - top}
145 |
146 | return
147 | }
148 |
149 | // MoveTo X and Y to x and y
150 | // 将X和Y移至x和y
151 | func (p *InputTouchPoint) MoveTo(x, y float64) {
152 | p.X = x
153 | p.Y = y
154 | }
155 |
156 | // CookiesToParams converts Cookies list to NetworkCookieParam list
157 | // 将Cookies列表转换为NetworkCookieParam列表
158 | func CookiesToParams(cookies []*NetworkCookie) []*NetworkCookieParam {
159 | list := []*NetworkCookieParam{}
160 | for _, c := range cookies {
161 | list = append(list, &NetworkCookieParam{
162 | Name: c.Name,
163 | Value: c.Value,
164 | Domain: c.Domain,
165 | Path: c.Path,
166 | Secure: c.Secure,
167 | HTTPOnly: c.HTTPOnly,
168 | SameSite: c.SameSite,
169 | Expires: c.Expires,
170 | Priority: c.Priority,
171 | })
172 | }
173 | return list
174 | }
175 |
--------------------------------------------------------------------------------
/lib/proto/a_utils.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | import (
4 | "regexp"
5 | "strings"
6 | )
7 |
8 | var regAsterisk = regexp.MustCompile(`([^\\])\*`)
9 | var regBackSlash = regexp.MustCompile(`([^\\])\?`)
10 |
11 | // PatternToReg FetchRequestPattern.URLPattern to regular expression
12 | // PatternToReg 将 FetchRequestPattern.URLPattern 转为正则表达式
13 | func PatternToReg(pattern string) string {
14 | if pattern == "" {
15 | return ""
16 | }
17 |
18 | pattern = " " + pattern
19 | pattern = regAsterisk.ReplaceAllString(pattern, "$1.*")
20 | pattern = regBackSlash.ReplaceAllString(pattern, "$1.")
21 |
22 | return `\A` + strings.TrimSpace(pattern) + `\z`
23 | }
24 |
--------------------------------------------------------------------------------
/lib/proto/a_utils_test.go:
--------------------------------------------------------------------------------
1 | package proto_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/go-rod/rod/lib/proto"
7 | "github.com/ysmood/got"
8 | )
9 |
10 | type T struct {
11 | got.G
12 | }
13 |
14 | func Test(t *testing.T) {
15 | got.Each(t, T{})
16 | }
17 |
18 | func (t T) PatternToReg() {
19 | t.Eq(``, proto.PatternToReg(""))
20 | t.Eq(`\A.*\z`, proto.PatternToReg("*"))
21 | t.Eq(`\A.\z`, proto.PatternToReg("?"))
22 | t.Eq(`\Aa\z`, proto.PatternToReg("a"))
23 | t.Eq(`\Aa.com/.*/test\z`, proto.PatternToReg("a.com/*/test"))
24 | t.Eq(`\A\?\*\z`, proto.PatternToReg(`\?\*`))
25 | t.Eq(`\Aa.com\?a=10&b=\*\z`, proto.PatternToReg(`a.com\?a=10&b=\*`))
26 | }
27 |
--------------------------------------------------------------------------------
/lib/proto/cast.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | /*
6 |
7 | Cast
8 |
9 | A domain for interacting with Cast, Presentation API, and Remote Playback API
10 | functionalities.
11 |
12 | */
13 |
14 | // CastSink ...
15 | type CastSink struct {
16 |
17 | // Name ...
18 | Name string `json:"name"`
19 |
20 | // ID ...
21 | ID string `json:"id"`
22 |
23 | // Session (optional) Text describing the current session. Present only if there is an active
24 | // session on the sink.
25 | Session string `json:"session,omitempty"`
26 | }
27 |
28 | // CastEnable Starts observing for sinks that can be used for tab mirroring, and if set,
29 | // sinks compatible with |presentationUrl| as well. When sinks are found, a
30 | // |sinksUpdated| event is fired.
31 | // Also starts observing for issue messages. When an issue is added or removed,
32 | // an |issueUpdated| event is fired.
33 | type CastEnable struct {
34 |
35 | // PresentationURL (optional) ...
36 | PresentationURL string `json:"presentationUrl,omitempty"`
37 | }
38 |
39 | // ProtoReq name
40 | func (m CastEnable) ProtoReq() string { return "Cast.enable" }
41 |
42 | // Call sends the request
43 | func (m CastEnable) Call(c Client) error {
44 | return call(m.ProtoReq(), m, nil, c)
45 | }
46 |
47 | // CastDisable Stops observing for sinks and issues.
48 | type CastDisable struct {
49 | }
50 |
51 | // ProtoReq name
52 | func (m CastDisable) ProtoReq() string { return "Cast.disable" }
53 |
54 | // Call sends the request
55 | func (m CastDisable) Call(c Client) error {
56 | return call(m.ProtoReq(), m, nil, c)
57 | }
58 |
59 | // CastSetSinkToUse Sets a sink to be used when the web page requests the browser to choose a
60 | // sink via Presentation API, Remote Playback API, or Cast SDK.
61 | type CastSetSinkToUse struct {
62 |
63 | // SinkName ...
64 | SinkName string `json:"sinkName"`
65 | }
66 |
67 | // ProtoReq name
68 | func (m CastSetSinkToUse) ProtoReq() string { return "Cast.setSinkToUse" }
69 |
70 | // Call sends the request
71 | func (m CastSetSinkToUse) Call(c Client) error {
72 | return call(m.ProtoReq(), m, nil, c)
73 | }
74 |
75 | // CastStartDesktopMirroring Starts mirroring the desktop to the sink.
76 | type CastStartDesktopMirroring struct {
77 |
78 | // SinkName ...
79 | SinkName string `json:"sinkName"`
80 | }
81 |
82 | // ProtoReq name
83 | func (m CastStartDesktopMirroring) ProtoReq() string { return "Cast.startDesktopMirroring" }
84 |
85 | // Call sends the request
86 | func (m CastStartDesktopMirroring) Call(c Client) error {
87 | return call(m.ProtoReq(), m, nil, c)
88 | }
89 |
90 | // CastStartTabMirroring Starts mirroring the tab to the sink.
91 | type CastStartTabMirroring struct {
92 |
93 | // SinkName ...
94 | SinkName string `json:"sinkName"`
95 | }
96 |
97 | // ProtoReq name
98 | func (m CastStartTabMirroring) ProtoReq() string { return "Cast.startTabMirroring" }
99 |
100 | // Call sends the request
101 | func (m CastStartTabMirroring) Call(c Client) error {
102 | return call(m.ProtoReq(), m, nil, c)
103 | }
104 |
105 | // CastStopCasting Stops the active Cast session on the sink.
106 | type CastStopCasting struct {
107 |
108 | // SinkName ...
109 | SinkName string `json:"sinkName"`
110 | }
111 |
112 | // ProtoReq name
113 | func (m CastStopCasting) ProtoReq() string { return "Cast.stopCasting" }
114 |
115 | // Call sends the request
116 | func (m CastStopCasting) Call(c Client) error {
117 | return call(m.ProtoReq(), m, nil, c)
118 | }
119 |
120 | // CastSinksUpdated This is fired whenever the list of available sinks changes. A sink is a
121 | // device or a software surface that you can cast to.
122 | type CastSinksUpdated struct {
123 |
124 | // Sinks ...
125 | Sinks []*CastSink `json:"sinks"`
126 | }
127 |
128 | // ProtoEvent name
129 | func (evt CastSinksUpdated) ProtoEvent() string {
130 | return "Cast.sinksUpdated"
131 | }
132 |
133 | // CastIssueUpdated This is fired whenever the outstanding issue/error message changes.
134 | // |issueMessage| is empty if there is no issue.
135 | type CastIssueUpdated struct {
136 |
137 | // IssueMessage ...
138 | IssueMessage string `json:"issueMessage"`
139 | }
140 |
141 | // ProtoEvent name
142 | func (evt CastIssueUpdated) ProtoEvent() string {
143 | return "Cast.issueUpdated"
144 | }
145 |
--------------------------------------------------------------------------------
/lib/proto/database.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | import (
6 | "github.com/ysmood/gson"
7 | )
8 |
9 | /*
10 |
11 | Database
12 |
13 | */
14 |
15 | // DatabaseDatabaseID Unique identifier of Database object.
16 | type DatabaseDatabaseID string
17 |
18 | // DatabaseDatabase Database object.
19 | type DatabaseDatabase struct {
20 |
21 | // ID Database ID.
22 | ID DatabaseDatabaseID `json:"id"`
23 |
24 | // Domain Database domain.
25 | Domain string `json:"domain"`
26 |
27 | // Name Database name.
28 | Name string `json:"name"`
29 |
30 | // Version Database version.
31 | Version string `json:"version"`
32 | }
33 |
34 | // DatabaseError Database error.
35 | type DatabaseError struct {
36 |
37 | // Message Error message.
38 | Message string `json:"message"`
39 |
40 | // Code Error code.
41 | Code int `json:"code"`
42 | }
43 |
44 | // DatabaseDisable Disables database tracking, prevents database events from being sent to the client.
45 | type DatabaseDisable struct {
46 | }
47 |
48 | // ProtoReq name
49 | func (m DatabaseDisable) ProtoReq() string { return "Database.disable" }
50 |
51 | // Call sends the request
52 | func (m DatabaseDisable) Call(c Client) error {
53 | return call(m.ProtoReq(), m, nil, c)
54 | }
55 |
56 | // DatabaseEnable Enables database tracking, database events will now be delivered to the client.
57 | type DatabaseEnable struct {
58 | }
59 |
60 | // ProtoReq name
61 | func (m DatabaseEnable) ProtoReq() string { return "Database.enable" }
62 |
63 | // Call sends the request
64 | func (m DatabaseEnable) Call(c Client) error {
65 | return call(m.ProtoReq(), m, nil, c)
66 | }
67 |
68 | // DatabaseExecuteSQL ...
69 | type DatabaseExecuteSQL struct {
70 |
71 | // DatabaseID ...
72 | DatabaseID DatabaseDatabaseID `json:"databaseId"`
73 |
74 | // Query ...
75 | Query string `json:"query"`
76 | }
77 |
78 | // ProtoReq name
79 | func (m DatabaseExecuteSQL) ProtoReq() string { return "Database.executeSQL" }
80 |
81 | // Call the request
82 | func (m DatabaseExecuteSQL) Call(c Client) (*DatabaseExecuteSQLResult, error) {
83 | var res DatabaseExecuteSQLResult
84 | return &res, call(m.ProtoReq(), m, &res, c)
85 | }
86 |
87 | // DatabaseExecuteSQLResult ...
88 | type DatabaseExecuteSQLResult struct {
89 |
90 | // ColumnNames (optional) ...
91 | ColumnNames []string `json:"columnNames,omitempty"`
92 |
93 | // Values (optional) ...
94 | Values []gson.JSON `json:"values,omitempty"`
95 |
96 | // SQLError (optional) ...
97 | SQLError *DatabaseError `json:"sqlError,omitempty"`
98 | }
99 |
100 | // DatabaseGetDatabaseTableNames ...
101 | type DatabaseGetDatabaseTableNames struct {
102 |
103 | // DatabaseID ...
104 | DatabaseID DatabaseDatabaseID `json:"databaseId"`
105 | }
106 |
107 | // ProtoReq name
108 | func (m DatabaseGetDatabaseTableNames) ProtoReq() string { return "Database.getDatabaseTableNames" }
109 |
110 | // Call the request
111 | func (m DatabaseGetDatabaseTableNames) Call(c Client) (*DatabaseGetDatabaseTableNamesResult, error) {
112 | var res DatabaseGetDatabaseTableNamesResult
113 | return &res, call(m.ProtoReq(), m, &res, c)
114 | }
115 |
116 | // DatabaseGetDatabaseTableNamesResult ...
117 | type DatabaseGetDatabaseTableNamesResult struct {
118 |
119 | // TableNames ...
120 | TableNames []string `json:"tableNames"`
121 | }
122 |
123 | // DatabaseAddDatabase ...
124 | type DatabaseAddDatabase struct {
125 |
126 | // Database ...
127 | Database *DatabaseDatabase `json:"database"`
128 | }
129 |
130 | // ProtoEvent name
131 | func (evt DatabaseAddDatabase) ProtoEvent() string {
132 | return "Database.addDatabase"
133 | }
134 |
--------------------------------------------------------------------------------
/lib/proto/device_orientation.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | /*
6 |
7 | DeviceOrientation
8 |
9 | */
10 |
11 | // DeviceOrientationClearDeviceOrientationOverride Clears the overridden Device Orientation.
12 | type DeviceOrientationClearDeviceOrientationOverride struct {
13 | }
14 |
15 | // ProtoReq name
16 | func (m DeviceOrientationClearDeviceOrientationOverride) ProtoReq() string {
17 | return "DeviceOrientation.clearDeviceOrientationOverride"
18 | }
19 |
20 | // Call sends the request
21 | func (m DeviceOrientationClearDeviceOrientationOverride) Call(c Client) error {
22 | return call(m.ProtoReq(), m, nil, c)
23 | }
24 |
25 | // DeviceOrientationSetDeviceOrientationOverride Overrides the Device Orientation.
26 | type DeviceOrientationSetDeviceOrientationOverride struct {
27 |
28 | // Alpha Mock alpha
29 | Alpha float64 `json:"alpha"`
30 |
31 | // Beta Mock beta
32 | Beta float64 `json:"beta"`
33 |
34 | // Gamma Mock gamma
35 | Gamma float64 `json:"gamma"`
36 | }
37 |
38 | // ProtoReq name
39 | func (m DeviceOrientationSetDeviceOrientationOverride) ProtoReq() string {
40 | return "DeviceOrientation.setDeviceOrientationOverride"
41 | }
42 |
43 | // Call sends the request
44 | func (m DeviceOrientationSetDeviceOrientationOverride) Call(c Client) error {
45 | return call(m.ProtoReq(), m, nil, c)
46 | }
47 |
--------------------------------------------------------------------------------
/lib/proto/event_breakpoints.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | /*
6 |
7 | EventBreakpoints
8 |
9 | EventBreakpoints permits setting breakpoints on particular operations and
10 | events in targets that run JavaScript but do not have a DOM.
11 | JavaScript execution will stop on these operations as if there was a regular
12 | breakpoint set.
13 |
14 | */
15 |
16 | // EventBreakpointsSetInstrumentationBreakpoint Sets breakpoint on particular native event.
17 | type EventBreakpointsSetInstrumentationBreakpoint struct {
18 |
19 | // EventName Instrumentation name to stop on.
20 | EventName string `json:"eventName"`
21 | }
22 |
23 | // ProtoReq name
24 | func (m EventBreakpointsSetInstrumentationBreakpoint) ProtoReq() string {
25 | return "EventBreakpoints.setInstrumentationBreakpoint"
26 | }
27 |
28 | // Call sends the request
29 | func (m EventBreakpointsSetInstrumentationBreakpoint) Call(c Client) error {
30 | return call(m.ProtoReq(), m, nil, c)
31 | }
32 |
33 | // EventBreakpointsRemoveInstrumentationBreakpoint Removes breakpoint on particular native event.
34 | type EventBreakpointsRemoveInstrumentationBreakpoint struct {
35 |
36 | // EventName Instrumentation name to stop on.
37 | EventName string `json:"eventName"`
38 | }
39 |
40 | // ProtoReq name
41 | func (m EventBreakpointsRemoveInstrumentationBreakpoint) ProtoReq() string {
42 | return "EventBreakpoints.removeInstrumentationBreakpoint"
43 | }
44 |
45 | // Call sends the request
46 | func (m EventBreakpointsRemoveInstrumentationBreakpoint) Call(c Client) error {
47 | return call(m.ProtoReq(), m, nil, c)
48 | }
49 |
--------------------------------------------------------------------------------
/lib/proto/generate/patch.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ysmood/gson"
7 | )
8 |
9 | func patch(json gson.JSON) {
10 | k := func(k, v string) gson.Query {
11 | return func(obj interface{}) (val interface{}, has bool) {
12 | for _, el := range obj.([]interface{}) {
13 | res := el.(map[string]interface{})[k]
14 | if res == v {
15 | return el, true
16 | }
17 | }
18 | panic("not found")
19 | }
20 | }
21 | index := func(obj interface{}, k, v string) string {
22 | for i, el := range obj.([]interface{}) {
23 | res := el.(map[string]interface{})[k]
24 | if res == v {
25 | return fmt.Sprintf("%d", i)
26 | }
27 | }
28 | panic("not found")
29 | }
30 |
31 | getTypes := func(domain string) gson.JSON {
32 | res, _ := json.Gets("domains", k("domain", domain), "types")
33 | return res
34 | }
35 |
36 | // TargetTargetInfoType
37 | j, _ := getTypes("Target").Gets(k("id", "TargetInfo"), "properties", k("name", "type"))
38 | j.Set("enum", []string{
39 | "page", "background_page", "service_worker", "shared_worker", "browser", "other",
40 | })
41 |
42 | // PageLifecycleEventName
43 | j, _ = json.Gets("domains", k("domain", "Page"), "events", k("name", "lifecycleEvent"), "parameters", k("name", "name"))
44 | j.Set("enum", []string{
45 | "init", "firstPaint", "firstContentfulPaint", "firstImagePaint", "firstMeaningfulPaintCandidate",
46 | "DOMContentLoaded", "load", "networkAlmostIdle", "firstMeaningfulPaint", "networkIdle",
47 | })
48 |
49 | // replace these with better type definition
50 | j, _ = getTypes("Input").Gets(k("id", "TimeSinceEpoch"))
51 | j.Set("skip", true)
52 | j, _ = getTypes("Network").Gets(k("id", "TimeSinceEpoch"))
53 | j.Set("skip", true)
54 | j, _ = getTypes("Network").Gets(k("id", "MonotonicTime"))
55 | j.Set("skip", true)
56 |
57 | // fix Cookie.Expires
58 | j, _ = getTypes("Network").Gets(k("id", "Cookie"), "properties")
59 | j.Set(index(j.Val(), "name", "expires"), map[string]interface{}{
60 | "$ref": "TimeSinceEpoch",
61 | "description": "Cookie expiration date",
62 | "name": "expires",
63 | })
64 |
65 | // deltaX and deltaY are not optional for mouseWheel events
66 | j, _ = json.Gets("domains", k("domain", "Input"), "commands", k("name", "dispatchMouseEvent"), "parameters")
67 | jj, _ := j.Gets(k("name", "deltaX"))
68 | jj.Del("optional")
69 | jj, _ = j.Gets(k("name", "deltaY"))
70 | jj.Del("optional")
71 |
72 | // removing the optional for the body as we need to distinguish between no body and empty body
73 | // with that fix we can send an 'empty body' using `SetBody([]byte{})`
74 | // and 'no body' by not calling using 'SetBody()' on the response
75 | j, _ = json.Gets("domains", k("domain", "Fetch"), "commands", k("name", "fulfillRequest"), "parameters")
76 | jj, _ = j.Gets(k("name", "body"))
77 | jj.Del("optional")
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/lib/proto/generate/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http"
7 | "net/url"
8 | "regexp"
9 | "strings"
10 |
11 | "github.com/go-rod/rod/lib/launcher"
12 | "github.com/go-rod/rod/lib/utils"
13 | "github.com/ysmood/gson"
14 | )
15 |
16 | func getSchema() gson.JSON {
17 | l := launcher.New().Bin(launcher.NewBrowser().MustGet())
18 | defer l.Kill()
19 |
20 | u := l.MustLaunch()
21 | parsed, err := url.Parse(u)
22 | utils.E(err)
23 | parsed.Scheme = "http"
24 | parsed.Path = "/json/protocol"
25 |
26 | res, err := http.Get(parsed.String())
27 | utils.E(err)
28 | defer func() { _ = res.Body.Close() }()
29 |
30 | data, err := ioutil.ReadAll(res.Body)
31 | utils.E(err)
32 |
33 | obj := gson.New(data)
34 |
35 | utils.E(utils.OutputFile("tmp/proto.json", obj.JSON("", " ")))
36 |
37 | return obj
38 | }
39 |
40 | func mapType(n string) string {
41 | return map[string]string{
42 | "boolean": "bool",
43 | "number": "float64",
44 | "integer": "int",
45 | "string": "string",
46 | "binary": "[]byte",
47 | "object": "map[string]gson.JSON",
48 | "any": "gson.JSON",
49 | }[n]
50 | }
51 |
52 | func typeName(domain *domain, schema gson.JSON) string {
53 | typeName := ""
54 | if schema.Has("type") {
55 | typeName = schema.Get("type").Str()
56 | }
57 |
58 | if typeName == "array" {
59 | item := schema.Get("items")
60 |
61 | if item.Has("type") {
62 | typeName = "[]" + mapType(item.Get("type").Str())
63 | } else {
64 | ref := item.Get("$ref").Str()
65 | if domain.ref(ref) {
66 | typeName = "[]*" + refName(domain.name, ref)
67 | } else {
68 | typeName = "[]" + refName(domain.name, ref)
69 | }
70 | }
71 | } else if schema.Has("$ref") {
72 | ref := schema.Get("$ref").Str()
73 | if domain.ref(ref) {
74 | typeName += "*"
75 | }
76 | typeName += refName(domain.name, ref)
77 | } else {
78 | typeName = mapType(typeName)
79 | }
80 |
81 | switch typeName {
82 | case "NetworkTimeSinceEpoch", "InputTimeSinceEpoch":
83 | typeName = "TimeSinceEpoch"
84 | case "NetworkMonotonicTime":
85 | typeName = "MonotonicTime"
86 | }
87 |
88 | return typeName
89 | }
90 |
91 | func enumList(schema gson.JSON) []string {
92 | var enum []string
93 | if schema.Has("enum") {
94 | enum = []string{}
95 | for _, v := range schema.Get("enum").Arr() {
96 | if _, ok := v.Val().(string); !ok {
97 | panic("enum type error")
98 | }
99 | enum = append(enum, v.Str())
100 | }
101 | }
102 |
103 | return enum
104 | }
105 |
106 | func jsonTag(name string, optional bool) string {
107 | jsonTagValue := name
108 | if optional {
109 | jsonTagValue += ",omitempty"
110 | }
111 | return fmt.Sprintf("`json:\"%s\"`", jsonTagValue)
112 | }
113 |
114 | func refName(domain, id string) string {
115 | if strings.Contains(id, ".") {
116 | return symbol(id)
117 | }
118 | return domain + symbol(id)
119 | }
120 |
121 | // make sure golint works fine
122 | func symbol(n string) string {
123 | if n == "" {
124 | return ""
125 | }
126 |
127 | n = strings.ReplaceAll(n, ".", "")
128 |
129 | dashed := regexp.MustCompile(`[-_]`).Split(n, -1)
130 | if len(dashed) > 1 {
131 | converted := []string{}
132 | for _, part := range dashed {
133 | converted = append(converted, strings.ToUpper(part[:1])+part[1:])
134 | }
135 | n = strings.Join(converted, "")
136 | }
137 |
138 | n = strings.ToUpper(n[:1]) + n[1:]
139 |
140 | n = replaceLower(n, "Id")
141 | n = replaceLower(n, "Css")
142 | n = replaceLower(n, "Url")
143 | n = replaceLower(n, "Uuid")
144 | n = replaceLower(n, "Xml")
145 | n = replaceLower(n, "Http")
146 | n = replaceLower(n, "Dns")
147 | n = replaceLower(n, "Cpu")
148 | n = replaceLower(n, "Mime")
149 | n = replaceLower(n, "Json")
150 | n = replaceLower(n, "Html")
151 | n = replaceLower(n, "Guid")
152 | n = replaceLower(n, "Sql")
153 | n = replaceLower(n, "Eof")
154 | n = replaceLower(n, "Api")
155 |
156 | return n
157 | }
158 |
159 | func replaceLower(n, word string) string {
160 | return regexp.MustCompile(word+`([A-Z-_]|$)`).ReplaceAllStringFunc(n, strings.ToUpper)
161 | }
162 |
163 | var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
164 | var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
165 |
166 | func toSnakeCase(str string) string {
167 | snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
168 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
169 | return strings.ToLower(snake)
170 | }
171 |
--------------------------------------------------------------------------------
/lib/proto/inspector.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | /*
6 |
7 | Inspector
8 |
9 | */
10 |
11 | // InspectorDisable Disables inspector domain notifications.
12 | type InspectorDisable struct {
13 | }
14 |
15 | // ProtoReq name
16 | func (m InspectorDisable) ProtoReq() string { return "Inspector.disable" }
17 |
18 | // Call sends the request
19 | func (m InspectorDisable) Call(c Client) error {
20 | return call(m.ProtoReq(), m, nil, c)
21 | }
22 |
23 | // InspectorEnable Enables inspector domain notifications.
24 | type InspectorEnable struct {
25 | }
26 |
27 | // ProtoReq name
28 | func (m InspectorEnable) ProtoReq() string { return "Inspector.enable" }
29 |
30 | // Call sends the request
31 | func (m InspectorEnable) Call(c Client) error {
32 | return call(m.ProtoReq(), m, nil, c)
33 | }
34 |
35 | // InspectorDetached Fired when remote debugging connection is about to be terminated. Contains detach reason.
36 | type InspectorDetached struct {
37 |
38 | // Reason The reason why connection has been terminated.
39 | Reason string `json:"reason"`
40 | }
41 |
42 | // ProtoEvent name
43 | func (evt InspectorDetached) ProtoEvent() string {
44 | return "Inspector.detached"
45 | }
46 |
47 | // InspectorTargetCrashed Fired when debugging target has crashed
48 | type InspectorTargetCrashed struct {
49 | }
50 |
51 | // ProtoEvent name
52 | func (evt InspectorTargetCrashed) ProtoEvent() string {
53 | return "Inspector.targetCrashed"
54 | }
55 |
56 | // InspectorTargetReloadedAfterCrash Fired when debugging target has reloaded after crash
57 | type InspectorTargetReloadedAfterCrash struct {
58 | }
59 |
60 | // ProtoEvent name
61 | func (evt InspectorTargetReloadedAfterCrash) ProtoEvent() string {
62 | return "Inspector.targetReloadedAfterCrash"
63 | }
64 |
--------------------------------------------------------------------------------
/lib/proto/io.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | /*
6 |
7 | IO
8 |
9 | Input/Output operations for streams produced by DevTools.
10 |
11 | */
12 |
13 | // IOStreamHandle This is either obtained from another method or specified as `blob:<uuid>` where
14 | // `<uuid>` is an UUID of a Blob.
15 | type IOStreamHandle string
16 |
17 | // IOClose Close the stream, discard any temporary backing storage.
18 | type IOClose struct {
19 |
20 | // Handle Handle of the stream to close.
21 | Handle IOStreamHandle `json:"handle"`
22 | }
23 |
24 | // ProtoReq name
25 | func (m IOClose) ProtoReq() string { return "IO.close" }
26 |
27 | // Call sends the request
28 | func (m IOClose) Call(c Client) error {
29 | return call(m.ProtoReq(), m, nil, c)
30 | }
31 |
32 | // IORead Read a chunk of the stream
33 | type IORead struct {
34 |
35 | // Handle Handle of the stream to read.
36 | Handle IOStreamHandle `json:"handle"`
37 |
38 | // Offset (optional) Seek to the specified offset before reading (if not specificed, proceed with offset
39 | // following the last read). Some types of streams may only support sequential reads.
40 | Offset *int `json:"offset,omitempty"`
41 |
42 | // Size (optional) Maximum number of bytes to read (left upon the agent discretion if not specified).
43 | Size *int `json:"size,omitempty"`
44 | }
45 |
46 | // ProtoReq name
47 | func (m IORead) ProtoReq() string { return "IO.read" }
48 |
49 | // Call the request
50 | func (m IORead) Call(c Client) (*IOReadResult, error) {
51 | var res IOReadResult
52 | return &res, call(m.ProtoReq(), m, &res, c)
53 | }
54 |
55 | // IOReadResult ...
56 | type IOReadResult struct {
57 |
58 | // Base64Encoded (optional) Set if the data is base64-encoded
59 | Base64Encoded bool `json:"base64Encoded,omitempty"`
60 |
61 | // Data Data that were read.
62 | Data string `json:"data"`
63 |
64 | // EOF Set if the end-of-file condition occurred while reading.
65 | EOF bool `json:"eof"`
66 | }
67 |
68 | // IOResolveBlob Return UUID of Blob object specified by a remote object id.
69 | type IOResolveBlob struct {
70 |
71 | // ObjectID Object id of a Blob object wrapper.
72 | ObjectID RuntimeRemoteObjectID `json:"objectId"`
73 | }
74 |
75 | // ProtoReq name
76 | func (m IOResolveBlob) ProtoReq() string { return "IO.resolveBlob" }
77 |
78 | // Call the request
79 | func (m IOResolveBlob) Call(c Client) (*IOResolveBlobResult, error) {
80 | var res IOResolveBlobResult
81 | return &res, call(m.ProtoReq(), m, &res, c)
82 | }
83 |
84 | // IOResolveBlobResult ...
85 | type IOResolveBlobResult struct {
86 |
87 | // UUID UUID of the specified Blob.
88 | UUID string `json:"uuid"`
89 | }
90 |
--------------------------------------------------------------------------------
/lib/proto/performance.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | /*
6 |
7 | Performance
8 |
9 | */
10 |
11 | // PerformanceMetric Run-time execution metric.
12 | type PerformanceMetric struct {
13 |
14 | // Name Metric name.
15 | Name string `json:"name"`
16 |
17 | // Value Metric value.
18 | Value float64 `json:"value"`
19 | }
20 |
21 | // PerformanceDisable Disable collecting and reporting metrics.
22 | type PerformanceDisable struct {
23 | }
24 |
25 | // ProtoReq name
26 | func (m PerformanceDisable) ProtoReq() string { return "Performance.disable" }
27 |
28 | // Call sends the request
29 | func (m PerformanceDisable) Call(c Client) error {
30 | return call(m.ProtoReq(), m, nil, c)
31 | }
32 |
33 | // PerformanceEnableTimeDomain enum
34 | type PerformanceEnableTimeDomain string
35 |
36 | const (
37 | // PerformanceEnableTimeDomainTimeTicks enum const
38 | PerformanceEnableTimeDomainTimeTicks PerformanceEnableTimeDomain = "timeTicks"
39 |
40 | // PerformanceEnableTimeDomainThreadTicks enum const
41 | PerformanceEnableTimeDomainThreadTicks PerformanceEnableTimeDomain = "threadTicks"
42 | )
43 |
44 | // PerformanceEnable Enable collecting and reporting metrics.
45 | type PerformanceEnable struct {
46 |
47 | // TimeDomain (optional) Time domain to use for collecting and reporting duration metrics.
48 | TimeDomain PerformanceEnableTimeDomain `json:"timeDomain,omitempty"`
49 | }
50 |
51 | // ProtoReq name
52 | func (m PerformanceEnable) ProtoReq() string { return "Performance.enable" }
53 |
54 | // Call sends the request
55 | func (m PerformanceEnable) Call(c Client) error {
56 | return call(m.ProtoReq(), m, nil, c)
57 | }
58 |
59 | // PerformanceSetTimeDomainTimeDomain enum
60 | type PerformanceSetTimeDomainTimeDomain string
61 |
62 | const (
63 | // PerformanceSetTimeDomainTimeDomainTimeTicks enum const
64 | PerformanceSetTimeDomainTimeDomainTimeTicks PerformanceSetTimeDomainTimeDomain = "timeTicks"
65 |
66 | // PerformanceSetTimeDomainTimeDomainThreadTicks enum const
67 | PerformanceSetTimeDomainTimeDomainThreadTicks PerformanceSetTimeDomainTimeDomain = "threadTicks"
68 | )
69 |
70 | // PerformanceSetTimeDomain (deprecated) (experimental) Sets time domain to use for collecting and reporting duration metrics.
71 | // Note that this must be called before enabling metrics collection. Calling
72 | // this method while metrics collection is enabled returns an error.
73 | type PerformanceSetTimeDomain struct {
74 |
75 | // TimeDomain Time domain
76 | TimeDomain PerformanceSetTimeDomainTimeDomain `json:"timeDomain"`
77 | }
78 |
79 | // ProtoReq name
80 | func (m PerformanceSetTimeDomain) ProtoReq() string { return "Performance.setTimeDomain" }
81 |
82 | // Call sends the request
83 | func (m PerformanceSetTimeDomain) Call(c Client) error {
84 | return call(m.ProtoReq(), m, nil, c)
85 | }
86 |
87 | // PerformanceGetMetrics Retrieve current values of run-time metrics.
88 | type PerformanceGetMetrics struct {
89 | }
90 |
91 | // ProtoReq name
92 | func (m PerformanceGetMetrics) ProtoReq() string { return "Performance.getMetrics" }
93 |
94 | // Call the request
95 | func (m PerformanceGetMetrics) Call(c Client) (*PerformanceGetMetricsResult, error) {
96 | var res PerformanceGetMetricsResult
97 | return &res, call(m.ProtoReq(), m, &res, c)
98 | }
99 |
100 | // PerformanceGetMetricsResult ...
101 | type PerformanceGetMetricsResult struct {
102 |
103 | // Metrics Current values for run-time metrics.
104 | Metrics []*PerformanceMetric `json:"metrics"`
105 | }
106 |
107 | // PerformanceMetrics Current values of the metrics.
108 | type PerformanceMetrics struct {
109 |
110 | // Metrics Current values of the metrics.
111 | Metrics []*PerformanceMetric `json:"metrics"`
112 |
113 | // Title Timestamp title.
114 | Title string `json:"title"`
115 | }
116 |
117 | // ProtoEvent name
118 | func (evt PerformanceMetrics) ProtoEvent() string {
119 | return "Performance.metrics"
120 | }
121 |
--------------------------------------------------------------------------------
/lib/proto/performance_timeline.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | /*
6 |
7 | PerformanceTimeline
8 |
9 | Reporting of performance timeline events, as specified in
10 | https://w3c.github.io/performance-timeline/#dom-performanceobserver.
11 |
12 | */
13 |
14 | // PerformanceTimelineLargestContentfulPaint See https://github.com/WICG/LargestContentfulPaint and largest_contentful_paint.idl
15 | type PerformanceTimelineLargestContentfulPaint struct {
16 |
17 | // RenderTime ...
18 | RenderTime TimeSinceEpoch `json:"renderTime"`
19 |
20 | // LoadTime ...
21 | LoadTime TimeSinceEpoch `json:"loadTime"`
22 |
23 | // Size The number of pixels being painted.
24 | Size float64 `json:"size"`
25 |
26 | // ElementID (optional) The id attribute of the element, if available.
27 | ElementID string `json:"elementId,omitempty"`
28 |
29 | // URL (optional) The URL of the image (may be trimmed).
30 | URL string `json:"url,omitempty"`
31 |
32 | // NodeID (optional) ...
33 | NodeID DOMBackendNodeID `json:"nodeId,omitempty"`
34 | }
35 |
36 | // PerformanceTimelineLayoutShiftAttribution ...
37 | type PerformanceTimelineLayoutShiftAttribution struct {
38 |
39 | // PreviousRect ...
40 | PreviousRect *DOMRect `json:"previousRect"`
41 |
42 | // CurrentRect ...
43 | CurrentRect *DOMRect `json:"currentRect"`
44 |
45 | // NodeID (optional) ...
46 | NodeID DOMBackendNodeID `json:"nodeId,omitempty"`
47 | }
48 |
49 | // PerformanceTimelineLayoutShift See https://wicg.github.io/layout-instability/#sec-layout-shift and layout_shift.idl
50 | type PerformanceTimelineLayoutShift struct {
51 |
52 | // Value Score increment produced by this event.
53 | Value float64 `json:"value"`
54 |
55 | // HadRecentInput ...
56 | HadRecentInput bool `json:"hadRecentInput"`
57 |
58 | // LastInputTime ...
59 | LastInputTime TimeSinceEpoch `json:"lastInputTime"`
60 |
61 | // Sources ...
62 | Sources []*PerformanceTimelineLayoutShiftAttribution `json:"sources"`
63 | }
64 |
65 | // PerformanceTimelineTimelineEvent ...
66 | type PerformanceTimelineTimelineEvent struct {
67 |
68 | // FrameID Identifies the frame that this event is related to. Empty for non-frame targets.
69 | FrameID PageFrameID `json:"frameId"`
70 |
71 | // Type The event type, as specified in https://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype
72 | // This determines which of the optional "details" fiedls is present.
73 | Type string `json:"type"`
74 |
75 | // Name Name may be empty depending on the type.
76 | Name string `json:"name"`
77 |
78 | // Time Time in seconds since Epoch, monotonically increasing within document lifetime.
79 | Time TimeSinceEpoch `json:"time"`
80 |
81 | // Duration (optional) Event duration, if applicable.
82 | Duration *float64 `json:"duration,omitempty"`
83 |
84 | // LcpDetails (optional) ...
85 | LcpDetails *PerformanceTimelineLargestContentfulPaint `json:"lcpDetails,omitempty"`
86 |
87 | // LayoutShiftDetails (optional) ...
88 | LayoutShiftDetails *PerformanceTimelineLayoutShift `json:"layoutShiftDetails,omitempty"`
89 | }
90 |
91 | // PerformanceTimelineEnable Previously buffered events would be reported before method returns.
92 | // See also: timelineEventAdded
93 | type PerformanceTimelineEnable struct {
94 |
95 | // EventTypes The types of event to report, as specified in
96 | // https://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype
97 | // The specified filter overrides any previous filters, passing empty
98 | // filter disables recording.
99 | // Note that not all types exposed to the web platform are currently supported.
100 | EventTypes []string `json:"eventTypes"`
101 | }
102 |
103 | // ProtoReq name
104 | func (m PerformanceTimelineEnable) ProtoReq() string { return "PerformanceTimeline.enable" }
105 |
106 | // Call sends the request
107 | func (m PerformanceTimelineEnable) Call(c Client) error {
108 | return call(m.ProtoReq(), m, nil, c)
109 | }
110 |
111 | // PerformanceTimelineTimelineEventAdded Sent when a performance timeline event is added. See reportPerformanceTimeline method.
112 | type PerformanceTimelineTimelineEventAdded struct {
113 |
114 | // Event ...
115 | Event *PerformanceTimelineTimelineEvent `json:"event"`
116 | }
117 |
118 | // ProtoEvent name
119 | func (evt PerformanceTimelineTimelineEventAdded) ProtoEvent() string {
120 | return "PerformanceTimeline.timelineEventAdded"
121 | }
122 |
--------------------------------------------------------------------------------
/lib/proto/schema.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | /*
6 |
7 | Schema
8 |
9 | This domain is deprecated.
10 |
11 | */
12 |
13 | // SchemaDomain Description of the protocol domain.
14 | type SchemaDomain struct {
15 |
16 | // Name Domain name.
17 | Name string `json:"name"`
18 |
19 | // Version Domain version.
20 | Version string `json:"version"`
21 | }
22 |
23 | // SchemaGetDomains Returns supported domains.
24 | type SchemaGetDomains struct {
25 | }
26 |
27 | // ProtoReq name
28 | func (m SchemaGetDomains) ProtoReq() string { return "Schema.getDomains" }
29 |
30 | // Call the request
31 | func (m SchemaGetDomains) Call(c Client) (*SchemaGetDomainsResult, error) {
32 | var res SchemaGetDomainsResult
33 | return &res, call(m.ProtoReq(), m, &res, c)
34 | }
35 |
36 | // SchemaGetDomainsResult ...
37 | type SchemaGetDomainsResult struct {
38 |
39 | // Domains List of supported domains.
40 | Domains []*SchemaDomain `json:"domains"`
41 | }
42 |
--------------------------------------------------------------------------------
/lib/proto/tethering.go:
--------------------------------------------------------------------------------
1 | // This file is generated by "./lib/proto/generate"
2 |
3 | package proto
4 |
5 | /*
6 |
7 | Tethering
8 |
9 | The Tethering domain defines methods and events for browser port binding.
10 |
11 | */
12 |
13 | // TetheringBind Request browser port binding.
14 | type TetheringBind struct {
15 |
16 | // Port Port number to bind.
17 | Port int `json:"port"`
18 | }
19 |
20 | // ProtoReq name
21 | func (m TetheringBind) ProtoReq() string { return "Tethering.bind" }
22 |
23 | // Call sends the request
24 | func (m TetheringBind) Call(c Client) error {
25 | return call(m.ProtoReq(), m, nil, c)
26 | }
27 |
28 | // TetheringUnbind Request browser port unbinding.
29 | type TetheringUnbind struct {
30 |
31 | // Port Port number to unbind.
32 | Port int `json:"port"`
33 | }
34 |
35 | // ProtoReq name
36 | func (m TetheringUnbind) ProtoReq() string { return "Tethering.unbind" }
37 |
38 | // Call sends the request
39 | func (m TetheringUnbind) Call(c Client) error {
40 | return call(m.ProtoReq(), m, nil, c)
41 | }
42 |
43 | // TetheringAccepted Informs that port was successfully bound and got a specified connection id.
44 | type TetheringAccepted struct {
45 |
46 | // Port Port number that was successfully bound.
47 | Port int `json:"port"`
48 |
49 | // ConnectionID Connection id to be used.
50 | ConnectionID string `json:"connectionId"`
51 | }
52 |
53 | // ProtoEvent name
54 | func (evt TetheringAccepted) ProtoEvent() string {
55 | return "Tethering.accepted"
56 | }
57 |
--------------------------------------------------------------------------------
/lib/utils/check-cov/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/ysmood/got"
8 | )
9 |
10 | func main() {
11 | err := got.EnsureCoverage("coverage.out", 100)
12 | if err != nil {
13 | fmt.Println(err)
14 | os.Exit(1)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/utils/check-issue/body-invalid.txt:
--------------------------------------------------------------------------------
1 | Rod Version: v0.0.0
2 |
3 | ## The code to demonstrate your question
4 |
5 | ```
6 | test
7 | ```
8 |
9 | ```go
10 | func TestLab(t *testing.T) {
11 | g := setup(t)
12 | g.Eq(1, 1)
13 |
14 | ```
15 |
16 | ## What you expected to see
17 |
18 | Such as what you want to do.
19 |
20 | ## What you instead got
21 |
22 | Such as what error you see.
23 |
24 | ## What have you tried to solve the question
25 |
26 | Such as after modifying some source code of Rod you are able to get rid of the problem.
27 |
--------------------------------------------------------------------------------
/lib/utils/check-issue/body.txt:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Title of your question.
4 | title: ''
5 | labels: question
6 | assignees: ''
7 | ---
8 |
9 | Rod Version: v1.0.0
10 |
11 | ## The code to demonstrate your question
12 |
13 | 1. Clone Rod to your local and cd to the repository:
14 |
15 | ```bash
16 | git clone https://github.com/go-rod/rod
17 | cd rod
18 | ```
19 |
20 | 1. Use your code to replace the content of function `TestLab` in file `lab_test.go`.
21 |
22 | 1. Test your code with: `go test -run TestLab`.
23 |
24 | 1. Replace this section with your entire `lab_test.go` content, like below:
25 |
26 | ```go
27 | func TestLab(t *testing.T) {
28 | g := setup(t)
29 | g.Eq(1, 1)
30 | }
31 | ```
32 |
33 | ## What you expected to see
34 |
35 | Such as what you want to do.
36 |
37 | ## What you instead got
38 |
39 | Such as what error you see.
40 |
41 | ## What have you tried to solve the question
42 |
43 | Such as after modifying some source code of Rod you are able to get rid of the problem.
44 |
--------------------------------------------------------------------------------
/lib/utils/check-issue/check_format.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "go/parser"
8 | "go/scanner"
9 | "go/token"
10 | "os/exec"
11 | "regexp"
12 | "strings"
13 | )
14 |
15 | func checkMarkdown(body string) error {
16 | cmd := strings.Split("npx -ys -- markdownlint-cli@0.31.1 -s --disable MD041 MD034 MD013 MD047 MD010", " ")
17 | c := exec.Command(cmd[0], cmd[1:]...)
18 | c.Stdin = bytes.NewBufferString(body)
19 | b, err := c.CombinedOutput()
20 | if err == nil {
21 | return nil
22 | }
23 |
24 | b = regexp.MustCompile(`(?m)^stdin:`).ReplaceAll(b, []byte{})
25 |
26 | return fmt.Errorf("Please fix the format of your markdown:\n\n```txt\n%s```", b)
27 | }
28 |
29 | func checkGoCode(body string) error {
30 | reg := regexp.MustCompile("(?s)```go\r?\n(.+?)```")
31 |
32 | errs := []string{}
33 | i := 0
34 | for _, m := range reg.FindAllStringSubmatch(body, -1) {
35 | code := formatCode(m[1])
36 | _, err := parser.ParseFile(token.NewFileSet(), "", code, parser.AllErrors)
37 | if list, ok := err.(scanner.ErrorList); ok {
38 | i++
39 | errs = append(errs, fmt.Sprintf("@@ golang markdown block %d @@", i))
40 | for _, err := range list {
41 | errs = append(errs, err.Error())
42 | }
43 | }
44 | }
45 |
46 | if len(errs) != 0 {
47 | return errors.New("Please fix the golang code in your markdown:\n\n```txt\n" + strings.Join(errs, "\n") + "\n```")
48 | }
49 |
50 | return nil
51 | }
52 |
53 | func formatCode(code string) string {
54 | code = strings.TrimSpace(code)
55 | if strings.HasPrefix(code, "package ") {
56 | } else if strings.Contains(code, "func ") {
57 | code = "package main\n" + vars(code) + code
58 | } else {
59 | code = "package main\n" + vars(code) + "func main() {\n" + code + "\n}"
60 | }
61 |
62 | return code
63 | }
64 |
65 | func vars(code string) string {
66 | vars := ""
67 | if strings.Contains(code, "page.") && !strings.Contains(code, "page :=") {
68 | vars += "var page *rod.Page\n"
69 | }
70 | if strings.Contains(code, "browser.") && !strings.Contains(code, "browser :=") {
71 | vars += "var browser *rod.Browser\n"
72 | }
73 | return vars
74 | }
75 |
--------------------------------------------------------------------------------
/lib/utils/check-issue/check_version.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http"
7 | "regexp"
8 |
9 | "github.com/go-rod/rod/lib/utils"
10 | "github.com/ysmood/gson"
11 | )
12 |
13 | func checkVersion(body string) error {
14 | m := regexp.MustCompile(`Rod Version: v[0-9.]+`).FindString(body)
15 | if m == "" || m == "Rod Version: v0.0.0" {
16 | return fmt.Errorf(
17 | "Please add a valid `Rod Version: v0.0.0` to your issue. Current version is %s",
18 | currentVer(),
19 | )
20 | }
21 |
22 | return nil
23 | }
24 |
25 | func currentVer() string {
26 | q := req("/repos/go-rod/rod/tags?per_page=1")
27 | res, err := http.DefaultClient.Do(q)
28 | utils.E(err)
29 | defer func() { _ = res.Body.Close() }()
30 |
31 | data, err := ioutil.ReadAll(res.Body)
32 | utils.E(err)
33 |
34 | currentVer := gson.New(data).Get("0.name").Str()
35 |
36 | return currentVer
37 | }
38 |
--------------------------------------------------------------------------------
/lib/utils/check-issue/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-rod/rod/lib/utils/check-issue
2 |
3 | go 1.18
4 |
--------------------------------------------------------------------------------
/lib/utils/check-issue/main.go:
--------------------------------------------------------------------------------
1 | // The .github/workflows/check-issues.yml will use it as an github action
2 | // To test it locally, you can generate a personal github token: https://github.com/settings/tokens
3 | // Then run this:
4 | // ROD_GITHUB_ROBOT=your_token go run ./lib/utils/check-issue
5 |
6 | package main
7 |
8 | import (
9 | "bytes"
10 | "fmt"
11 | "io/ioutil"
12 | "log"
13 | "net/http"
14 | "os"
15 | "strings"
16 |
17 | "github.com/go-rod/rod/lib/utils"
18 | "github.com/ysmood/gson"
19 | )
20 |
21 | func main() {
22 | id, body := getIssue()
23 |
24 | log.Println("check issue", id)
25 |
26 | msg := check(body)
27 |
28 | deleteComments(id)
29 |
30 | if msg != "" {
31 | sendComment(id, msg)
32 | }
33 | }
34 |
35 | func check(body string) string {
36 | msg := []string{}
37 |
38 | for _, check := range []func(string) error{
39 | checkVersion, checkMarkdown, checkGoCode,
40 | } {
41 | err := check(body)
42 | if err != nil {
43 | msg = append(msg, err.Error())
44 | }
45 | }
46 | if len(msg) != 0 {
47 | return strings.Join(msg, "\n\n")
48 | }
49 | return ""
50 | }
51 |
52 | func getIssue() (int, string) {
53 | data, err := os.Open(os.Getenv("GITHUB_EVENT_PATH"))
54 | utils.E(err)
55 |
56 | issue := gson.New(data).Get("issue")
57 |
58 | id := issue.Get("number").Int()
59 | body := issue.Get("body").Str()
60 |
61 | return id, body
62 | }
63 |
64 | func sendComment(id int, msg string) {
65 | msg += fmt.Sprintf(
66 | "\n\n_generated by [check-issue](https://github.com/go-rod/rod/actions/runs/%s)_",
67 | os.Getenv("GITHUB_RUN_ID"),
68 | )
69 |
70 | q := req(fmt.Sprintf("/repos/go-rod/rod/issues/%d/comments", id))
71 | q.Method = http.MethodPost
72 | q.Body = ioutil.NopCloser(bytes.NewBuffer(utils.MustToJSONBytes(map[string]string{"body": msg})))
73 | res, err := http.DefaultClient.Do(q)
74 | utils.E(err)
75 | defer func() { _ = res.Body.Close() }()
76 | resE(res)
77 | }
78 |
79 | func deleteComments(id int) {
80 | q := req(fmt.Sprintf("/repos/go-rod/rod/issues/%d/comments", id))
81 | res, err := http.DefaultClient.Do(q)
82 | utils.E(err)
83 | resE(res)
84 |
85 | list := gson.New(res.Body)
86 |
87 | for _, c := range list.Arr() {
88 | if c.Get("user.login").Str() == "rod-robot" &&
89 | strings.Contains(c.Get("body").Str(), "[check-issue]") {
90 | iid := c.Get("id").Int()
91 | q := req(fmt.Sprintf("/repos/go-rod/rod/issues/comments/%d", iid))
92 | q.Method = http.MethodDelete
93 | res, err := http.DefaultClient.Do(q)
94 | utils.E(err)
95 | resE(res)
96 | }
97 | }
98 | }
99 |
100 | func req(u string) *http.Request {
101 | token := os.Getenv("ROD_GITHUB_ROBOT")
102 | if token == "" {
103 | panic("missing github token")
104 | }
105 |
106 | r, err := http.NewRequest(http.MethodGet, "https://api.github.com"+u, nil)
107 | utils.E(err)
108 | r.Header.Add("Authorization", "token "+token)
109 | return r
110 | }
111 |
112 | func resE(res *http.Response) {
113 | if res.StatusCode >= 400 {
114 | str, err := ioutil.ReadAll(res.Body)
115 | utils.E(err)
116 | panic(string(str))
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/lib/utils/check-issue/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "github.com/ysmood/got"
8 | )
9 |
10 | func TestBasic(t *testing.T) {
11 | g := got.T(t)
12 |
13 | _ = os.Setenv("ROD_GITHUB_ROBOT", "1234")
14 |
15 | body := g.Read(g.Open(false, "body-invalid.txt")).String()
16 |
17 | g.Eq(check(body), ""+
18 | "Please add a valid `Rod Version: v0.0.0` to your issue. Current version is \n"+
19 | "\n"+
20 | "Please fix the format of your markdown:\n"+
21 | "\n"+
22 | "```txt\n"+
23 | "5 MD040/fenced-code-language Fenced code blocks should have a language specified [Context: \"```\"]\n"+
24 | "20:24 MD009/no-trailing-spaces Trailing spaces [Expected: 0 or 2; Actual: 1]\n"+
25 | "```\n"+
26 | "\n"+
27 | "Please fix the golang code in your markdown:\n"+
28 | "\n"+
29 | "```txt\n"+
30 | "@@ golang markdown block 1 @@\n"+
31 | "4:15: expected ';', found 'EOF'\n"+
32 | "4:15: expected '}', found 'EOF'\n"+
33 | "```")
34 |
35 | body = g.Read(g.Open(false, "body.txt")).String()
36 | g.Zero(check(body))
37 | }
38 |
--------------------------------------------------------------------------------
/lib/utils/docker/main.go:
--------------------------------------------------------------------------------
1 | // The .github/workflows/docker.yml uses it as an github action
2 | // and run it like this:
3 | // DOCKER_TOKEN=$TOKEN go run ./lib/utils/docker $GITHUB_REF
4 | package main
5 |
6 | import (
7 | "fmt"
8 | "os"
9 | "regexp"
10 | "strings"
11 |
12 | "github.com/go-rod/rod/lib/utils"
13 | )
14 |
15 | const registry = "ghcr.io"
16 | const image = registry + "/go-rod/rod"
17 | const devImage = image + ":dev"
18 |
19 | var token = os.Getenv("DOCKER_TOKEN")
20 |
21 | func main() {
22 | event := os.Args[1]
23 |
24 | fmt.Println("Event:", event)
25 |
26 | master := regexp.MustCompile(`^refs/heads/master$`).MatchString(event)
27 | m := regexp.MustCompile(`^refs/tags/(v[0-9]+\.[0-9]+\.[0-9]+)$`).FindStringSubmatch(event)
28 | ver := ""
29 | if len(m) > 1 {
30 | ver = m[1]
31 | }
32 |
33 | if master {
34 | releaseLatest()
35 | } else if ver != "" {
36 | releaseWithVer(ver)
37 | } else {
38 | test()
39 | }
40 | }
41 |
42 | func releaseLatest() {
43 | login()
44 | test()
45 | utils.Exec("docker push", image)
46 | utils.Exec("docker push", devImage)
47 | }
48 |
49 | func releaseWithVer(ver string) {
50 | login()
51 |
52 | verImage := image + ":" + ver
53 |
54 | utils.Exec("docker pull", image)
55 | utils.Exec("docker tag", image, verImage)
56 | utils.Exec("docker push", verImage)
57 |
58 | utils.Exec("docker pull", devImage)
59 | utils.Exec("docker tag", devImage, verImage+"-dev")
60 | utils.Exec("docker push", verImage+"-dev")
61 | }
62 |
63 | func test() {
64 | utils.Exec("docker build -f=lib/docker/Dockerfile -t", image, description(false), ".")
65 | utils.Exec("docker build -f=lib/docker/dev.Dockerfile -t", devImage, description(true), ".")
66 |
67 | wd, err := os.Getwd()
68 | utils.E(err)
69 |
70 | utils.Exec("docker run", image, "rod-manager", "-h")
71 | utils.Exec("docker run -w=/t -v", fmt.Sprintf("%s:/t", wd), devImage, "go", "test")
72 | }
73 |
74 | func login() {
75 | utils.Exec("docker login -u=rod-robot", "-p", token, registry)
76 | }
77 |
78 | func description(dev bool) string {
79 | sha := strings.TrimSpace(utils.ExecLine(false, "git", "rev-parse", "HEAD"))
80 |
81 | f := "Dockerfile"
82 | if dev {
83 | f = "dev." + f
84 | }
85 |
86 | return `--label=org.opencontainers.image.description=https://github.com/go-rod/rod/blob/` + sha + "/lib/docker/" + f
87 | }
88 |
--------------------------------------------------------------------------------
/lib/utils/get-browser/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-rod/rod/lib/launcher"
7 | "github.com/go-rod/rod/lib/utils"
8 | )
9 |
10 | func main() {
11 | p, err := launcher.NewBrowser().Get()
12 | utils.E(err)
13 |
14 | fmt.Println(p)
15 | }
16 |
--------------------------------------------------------------------------------
/lib/utils/lint/eslint.yml:
--------------------------------------------------------------------------------
1 | extends:
2 | - eslint:recommended
3 | env:
4 | browser: true
5 | es6: true
6 | parserOptions:
7 | ecmaVersion: 2018
8 | plugins:
9 | - html
10 |
--------------------------------------------------------------------------------
/lib/utils/lint/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/go-rod/rod/lib/utils"
8 | )
9 |
10 | func main() {
11 | defer func() {
12 | if err := recover(); err != nil {
13 | fmt.Println(err)
14 | os.Exit(1)
15 | }
16 | }()
17 |
18 | utils.Exec("npx -ys -- eslint@8.7.0 --config=lib/utils/lint/eslint.yml --ext=.js,.html --fix --ignore-path=.gitignore .")
19 |
20 | utils.Exec("npx -ys -- prettier@2.5.1 --loglevel=error --config=lib/utils/lint/prettier.yml --write --ignore-path=.gitignore .")
21 |
22 | utils.Exec("go run github.com/ysmood/golangci-lint@latest")
23 |
24 | lintMustPrefix()
25 |
26 | checkGitClean()
27 | }
28 |
29 | func checkGitClean() {
30 | out := utils.ExecLine(false, "git status --porcelain")
31 | if out != "" {
32 | panic("Please run \"go generate\" on local and git commit the changes:\n" + out)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/utils/lint/prefix.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "go/ast"
5 | "go/parser"
6 | "go/token"
7 | "log"
8 | "path/filepath"
9 | "strings"
10 |
11 | "github.com/go-rod/rod/lib/utils"
12 | )
13 |
14 | func lintMustPrefix() {
15 | log.Println("[lint] the prefix 'Must'")
16 |
17 | paths, err := filepath.Glob("*.go")
18 | utils.E(err)
19 | lintErr := false
20 |
21 | for _, p := range paths {
22 | name := filepath.Base(p)
23 | if name == "must.go" || strings.HasSuffix(name, "_test.go") {
24 | continue
25 | }
26 |
27 | src, err := utils.ReadString(p)
28 | utils.E(err)
29 |
30 | list := token.NewFileSet()
31 | f, err := parser.ParseFile(list, p, src, 0)
32 | if err != nil {
33 | panic(err)
34 | }
35 |
36 | for _, decl := range f.Decls {
37 | fd, ok := decl.(*ast.FuncDecl)
38 | if ok && strings.HasPrefix(fd.Name.Name, "Must") {
39 | log.Printf("%s %s\n", list.Position(fd.Name.Pos()), fd.Name.Name)
40 | lintErr = true
41 | }
42 | }
43 | break
44 | }
45 |
46 | if lintErr {
47 | log.Fatalln("'Must' prefixed function should be declared in file 'must.go'")
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/utils/lint/prettier.yml:
--------------------------------------------------------------------------------
1 | semi: false
2 | singleQuote: true
3 | trailingComma: none
4 |
--------------------------------------------------------------------------------
/lib/utils/rename/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os/exec"
7 | "regexp"
8 | "strconv"
9 | "strings"
10 | "sync"
11 |
12 | "github.com/go-rod/rod/lib/utils"
13 | )
14 |
15 | func main() {
16 | rename()
17 | }
18 |
19 | func rename() {
20 | wg := sync.WaitGroup{}
21 | for _, p := range cmd("gopls -remote auto workspace_symbol .C.got") {
22 | pType := repose(p, -1, 4)
23 |
24 | cmd("gopls -remote auto rename -w " + pType + " T")
25 |
26 | wg.Add(1)
27 | go func() {
28 | for _, p := range cmd("gopls -remote auto references " + pType) {
29 | if strings.Contains(p, "definitions_test.go") {
30 | continue
31 | }
32 |
33 | pRef := repose(p, 0, -2)
34 | cmd("gopls -remote auto rename -w " + pRef + " t")
35 | }
36 | wg.Done()
37 | }()
38 | }
39 | wg.Wait()
40 | }
41 |
42 | var regPos = regexp.MustCompile(`^(.+?):(\d+):(\d+)-\d+`)
43 |
44 | func repose(raw string, lineOffset, columnOffset int) string {
45 | ms := regPos.FindStringSubmatch(raw)
46 |
47 | if ms == nil {
48 | log.Println("doesn't match", raw)
49 | return ""
50 | }
51 |
52 | p := ms[1]
53 |
54 | line, err := strconv.ParseInt(ms[2], 10, 64)
55 | utils.E(err)
56 |
57 | col, err := strconv.ParseInt(ms[3], 10, 64)
58 | utils.E(err)
59 |
60 | return fmt.Sprintf("%s:%d:%d", p, int(line)+lineOffset, int(col)+columnOffset)
61 | }
62 |
63 | func cmd(c string) []string {
64 | fmt.Println(c)
65 | args := strings.Split(c, " ")
66 | b, _ := exec.Command(args[0], args[1:]...).CombinedOutput()
67 | fmt.Println(string(b))
68 | return strings.Split(strings.TrimSpace(string(b)), "\n")
69 | }
70 |
--------------------------------------------------------------------------------
/lib/utils/setup/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os/exec"
6 |
7 | "github.com/go-rod/rod/lib/utils"
8 | )
9 |
10 | func main() {
11 | log.Println("setup project...")
12 |
13 | nodejsDeps()
14 |
15 | genDockerIgnore()
16 | }
17 |
18 | func nodejsDeps() {
19 | _, err := exec.LookPath("npm")
20 | if err != nil {
21 | log.Fatalln("please install Node.js: https://nodejs.org")
22 | }
23 |
24 | utils.Exec("npm i -s eslint-plugin-html")
25 | }
26 |
27 | func genDockerIgnore() {
28 | s, err := utils.ReadString(".gitignore")
29 | utils.E(err)
30 | utils.E(utils.OutputFile(".dockerignore", s))
31 | }
32 |
--------------------------------------------------------------------------------
/lib/utils/simple-check/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/go-rod/rod/lib/utils"
4 |
5 | func main() {
6 | utils.Exec("go run github.com/ysmood/golangci-lint@latest")
7 |
8 | utils.Exec("go test -coverprofile=coverage.out ./lib/launcher")
9 | utils.Exec("go run ./lib/utils/check-cov")
10 |
11 | utils.Exec("go test -coverprofile=coverage.out")
12 | utils.Exec("go run ./lib/utils/check-cov")
13 | }
14 |
--------------------------------------------------------------------------------
/lib/utils/sleeper.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | mr "math/rand"
7 | "reflect"
8 | "sync"
9 | "time"
10 | )
11 |
12 | // Sleep the goroutine for specified seconds, such as 2.3 seconds
13 | func Sleep(seconds float64) {
14 | d := time.Duration(seconds * float64(time.Second))
15 | time.Sleep(d)
16 | }
17 |
18 | // Sleeper sleeps the current gouroutine for sometime, returns the reason to wake, if ctx is done release resource
19 | type Sleeper func(context.Context) error
20 |
21 | // ErrMaxSleepCount type
22 | type ErrMaxSleepCount struct {
23 | // Max count
24 | Max int
25 | }
26 |
27 | // Error interface
28 | func (e *ErrMaxSleepCount) Error() string {
29 | return fmt.Sprintf("max sleep count %d exceeded", e.Max)
30 | }
31 |
32 | // Is interface
33 | func (e *ErrMaxSleepCount) Is(err error) bool {
34 | return reflect.TypeOf(e) == reflect.TypeOf(err)
35 | }
36 |
37 | // CountSleeper wakes immediately. When counts to the max returns *ErrMaxSleepCount
38 | func CountSleeper(max int) Sleeper {
39 | l := sync.Mutex{}
40 | count := 0
41 |
42 | return func(ctx context.Context) error {
43 | l.Lock()
44 | defer l.Unlock()
45 |
46 | if ctx.Err() != nil {
47 | return ctx.Err()
48 | }
49 |
50 | if count == max {
51 | return &ErrMaxSleepCount{max}
52 | }
53 | count++
54 | return nil
55 | }
56 | }
57 |
58 | // DefaultBackoff algorithm: A(n) = A(n-1) * random[1.9, 2.1)
59 | func DefaultBackoff(interval time.Duration) time.Duration {
60 | scale := 2 + (mr.Float64()-0.5)*0.2
61 | return time.Duration(float64(interval) * scale)
62 | }
63 |
64 | // BackoffSleeper returns a sleeper that sleeps in a backoff manner every time get called.
65 | // The sleep interval of the sleeper will grow from initInterval to maxInterval by the specified algorithm, then use maxInterval as the interval.
66 | // If maxInterval is not greater than 0, the sleeper will wake immediately.
67 | // If algorithm is nil, DefaultBackoff will be used.
68 | func BackoffSleeper(initInterval, maxInterval time.Duration, algorithm func(time.Duration) time.Duration) Sleeper {
69 | l := sync.Mutex{}
70 |
71 | if algorithm == nil {
72 | algorithm = DefaultBackoff
73 | }
74 |
75 | return func(ctx context.Context) error {
76 | l.Lock()
77 | defer l.Unlock()
78 |
79 | // wake immediately
80 | if maxInterval <= 0 {
81 | return nil
82 | }
83 |
84 | var interval time.Duration
85 | if initInterval < maxInterval {
86 | interval = algorithm(initInterval)
87 | } else {
88 | interval = maxInterval
89 | }
90 |
91 | t := time.NewTimer(interval)
92 | defer t.Stop()
93 |
94 | select {
95 | case <-ctx.Done():
96 | return ctx.Err()
97 | case <-t.C:
98 | initInterval = interval
99 | }
100 |
101 | return nil
102 | }
103 | }
104 |
105 | // EachSleepers returns a sleeper wakes up when each sleeper is awake.
106 | // If a sleeper returns error, it will wake up immediately.
107 | func EachSleepers(list ...Sleeper) Sleeper {
108 | return func(ctx context.Context) (err error) {
109 | for _, s := range list {
110 | err = s(ctx)
111 | if err != nil {
112 | break
113 | }
114 | }
115 |
116 | return
117 | }
118 | }
119 |
120 | // RaceSleepers returns a sleeper wakes up when one of the sleepers wakes.
121 | func RaceSleepers(list ...Sleeper) Sleeper {
122 | return func(ctx context.Context) error {
123 | ctx, cancel := context.WithCancel(ctx)
124 | done := make(chan error, len(list))
125 |
126 | sleep := func(s Sleeper) {
127 | done <- s(ctx)
128 | cancel()
129 | }
130 |
131 | for _, s := range list {
132 | go sleep(s)
133 | }
134 |
135 | return <-done
136 | }
137 | }
138 |
139 | // Retry fn and sleeper until fn returns true or s returns error
140 | func Retry(ctx context.Context, s Sleeper, fn func() (stop bool, err error)) error {
141 | for {
142 | stop, err := fn()
143 | if stop {
144 | return err
145 | }
146 | err = s(ctx)
147 | if err != nil {
148 | return err
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/lib/utils/sleeper_test.go:
--------------------------------------------------------------------------------
1 | package utils_test
2 |
3 | import (
4 | "context"
5 | "io"
6 | "testing"
7 | "time"
8 |
9 | "github.com/go-rod/rod/lib/utils"
10 | )
11 |
12 | func TestBackoffSleeperWakeNow(t *testing.T) {
13 | g := setup(t)
14 |
15 | g.E(utils.BackoffSleeper(0, 0, nil)(g.Context()))
16 | }
17 |
18 | func TestRetry(t *testing.T) {
19 | g := setup(t)
20 |
21 | count := 0
22 | s1 := utils.BackoffSleeper(1, 5, nil)
23 |
24 | err := utils.Retry(g.Context(), s1, func() (bool, error) {
25 | if count > 5 {
26 | return true, io.EOF
27 | }
28 | count++
29 | return false, nil
30 | })
31 |
32 | g.Eq(err.Error(), io.EOF.Error())
33 | }
34 |
35 | func TestRetryCancel(t *testing.T) {
36 | g := setup(t)
37 |
38 | ctx := g.Context()
39 | go ctx.Cancel()
40 | s := utils.BackoffSleeper(time.Second, time.Second, nil)
41 |
42 | err := utils.Retry(ctx, s, func() (bool, error) {
43 | return false, nil
44 | })
45 |
46 | g.Eq(err.Error(), context.Canceled.Error())
47 | }
48 |
49 | func TestCountSleeperErr(t *testing.T) {
50 | g := setup(t)
51 |
52 | ctx := g.Context()
53 | s := utils.CountSleeper(5)
54 | for i := 0; i < 5; i++ {
55 | _ = s(ctx)
56 | }
57 | g.Err(s(ctx))
58 | }
59 |
60 | func TestCountSleeperCancel(t *testing.T) {
61 | g := setup(t)
62 |
63 | s := utils.CountSleeper(5)
64 | g.Eq(s(g.Timeout(0)), context.DeadlineExceeded)
65 | }
66 |
67 | func TestEachSleepers(t *testing.T) {
68 | g := setup(t)
69 |
70 | s1 := utils.BackoffSleeper(1, 5, nil)
71 | s2 := utils.CountSleeper(5)
72 | s := utils.EachSleepers(s1, s2)
73 |
74 | err := utils.Retry(context.Background(), s, func() (stop bool, err error) {
75 | return false, nil
76 | })
77 |
78 | g.Is(err, &utils.ErrMaxSleepCount{})
79 | g.Eq(err.Error(), "max sleep count 5 exceeded")
80 | }
81 |
82 | func TestRaceSleepers(t *testing.T) {
83 | g := setup(t)
84 |
85 | s1 := utils.BackoffSleeper(1, 5, nil)
86 | s2 := utils.CountSleeper(5)
87 | s := utils.RaceSleepers(s1, s2)
88 |
89 | err := utils.Retry(context.Background(), s, func() (stop bool, err error) {
90 | return false, nil
91 | })
92 |
93 | g.Is(err, &utils.ErrMaxSleepCount{})
94 | g.Eq(err.Error(), "max sleep count 5 exceeded")
95 | }
96 |
--------------------------------------------------------------------------------
/must_test.go:
--------------------------------------------------------------------------------
1 | package rod_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/go-rod/rod"
7 | "github.com/go-rod/rod/lib/proto"
8 | )
9 |
10 | func TestBrowserWithPanic(t *testing.T) {
11 | g := setup(t)
12 |
13 | var triggers int
14 | trigger := func(x interface{}) {
15 | triggers++
16 | panic(x)
17 | }
18 |
19 | browser := g.browser.Sleeper(rod.NotFoundSleeper).WithPanic(trigger)
20 | g.Panic(func() { browser.MustPage("____") })
21 | g.Eq(1, triggers)
22 |
23 | page := browser.MustPage(g.blank())
24 | defer page.MustClose()
25 |
26 | g.Panic(func() { page.MustElement("____") })
27 | g.Eq(2, triggers)
28 |
29 | el := page.MustElement("html")
30 |
31 | g.Panic(func() {
32 | g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
33 | el.MustClick()
34 | })
35 | g.Eq(3, triggers)
36 | }
37 |
38 | func TestPageWithPanic(t *testing.T) {
39 | g := setup(t)
40 |
41 | var triggers int
42 | trigger := func(x interface{}) {
43 | triggers++
44 | panic(x)
45 | }
46 |
47 | browser := g.browser.Sleeper(rod.NotFoundSleeper)
48 | g.Panic(func() { browser.MustPage("____") })
49 | g.Eq(0, triggers)
50 |
51 | page := browser.MustPage(g.blank()).WithPanic(trigger)
52 | defer page.MustClose()
53 |
54 | g.Panic(func() { page.MustElement("____") })
55 | g.Eq(1, triggers)
56 |
57 | el := page.MustElement("html")
58 |
59 | g.Panic(func() {
60 | g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
61 | el.MustClick()
62 | })
63 | g.Eq(2, triggers)
64 | }
65 |
66 | func TestElementWithPanic(t *testing.T) {
67 | g := setup(t)
68 |
69 | var triggers int
70 | trigger := func(x interface{}) {
71 | triggers++
72 | panic(x)
73 | }
74 |
75 | browser := g.browser.Sleeper(rod.NotFoundSleeper)
76 | g.Panic(func() { browser.MustPage("____") })
77 | g.Eq(0, triggers)
78 |
79 | page := browser.MustPage(g.blank())
80 | defer page.MustClose()
81 |
82 | g.Panic(func() { page.MustElement("____") })
83 | g.Eq(0, triggers)
84 |
85 | el := page.MustElement("html").WithPanic(trigger)
86 |
87 | g.Panic(func() {
88 | g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
89 | el.MustClick()
90 | })
91 | g.Eq(1, triggers)
92 | }
93 |
--------------------------------------------------------------------------------
/states.go:
--------------------------------------------------------------------------------
1 | package rod
2 |
3 | import (
4 | "reflect"
5 |
6 | "github.com/go-rod/rod/lib/proto"
7 | )
8 |
9 | type stateKey struct {
10 | browserContextID proto.BrowserBrowserContextID
11 | sessionID proto.TargetSessionID
12 | methodName string
13 | }
14 |
15 | func (b *Browser) key(sessionID proto.TargetSessionID, methodName string) stateKey {
16 | return stateKey{
17 | browserContextID: b.BrowserContextID,
18 | sessionID: sessionID,
19 | methodName: methodName,
20 | }
21 | }
22 |
23 | func (b *Browser) set(sessionID proto.TargetSessionID, methodName string, params interface{}) {
24 | b.states.Store(b.key(sessionID, methodName), params)
25 |
26 | key := ""
27 | switch methodName {
28 | case (proto.EmulationClearDeviceMetricsOverride{}).ProtoReq():
29 | key = (proto.EmulationSetDeviceMetricsOverride{}).ProtoReq()
30 | case (proto.EmulationClearGeolocationOverride{}).ProtoReq():
31 | key = (proto.EmulationSetGeolocationOverride{}).ProtoReq()
32 | default:
33 | domain, name := proto.ParseMethodName(methodName)
34 | if name == "disable" {
35 | key = domain + ".enable"
36 | }
37 | }
38 | if key != "" {
39 | b.states.Delete(b.key(sessionID, key))
40 | }
41 | }
42 |
43 | // LoadState into the method, seesionID can be empty.
44 | // 在方法的LoadState中,sessionID可以为空。
45 | func (b *Browser) LoadState(sessionID proto.TargetSessionID, method proto.Request) (has bool) {
46 | data, has := b.states.Load(b.key(sessionID, method.ProtoReq()))
47 | if has {
48 | reflect.Indirect(reflect.ValueOf(method)).Set(
49 | reflect.Indirect(reflect.ValueOf(data)),
50 | )
51 | }
52 | return
53 | }
54 |
55 | // RemoveState a state
56 | // 删除一个 state
57 | func (b *Browser) RemoveState(key interface{}) {
58 | b.states.Delete(key)
59 | }
60 |
61 | // EnableDomain and returns a restore function to restore previous state
62 | // EnableDomain 返回一个恢复函数来恢复之前的 State
63 | func (b *Browser) EnableDomain(sessionID proto.TargetSessionID, req proto.Request) (restore func()) {
64 | _, enabled := b.states.Load(b.key(sessionID, req.ProtoReq()))
65 |
66 | if !enabled {
67 | _, _ = b.Call(b.ctx, string(sessionID), req.ProtoReq(), req)
68 | }
69 |
70 | return func() {
71 | if !enabled {
72 | domain, _ := proto.ParseMethodName(req.ProtoReq())
73 | _, _ = b.Call(b.ctx, string(sessionID), domain+".disable", nil)
74 | }
75 | }
76 | }
77 |
78 | // DisableDomain and returns a restore function to restore previous state
79 | // DisableDomain 返回一个恢复函数来恢复之前的状态
80 | func (b *Browser) DisableDomain(sessionID proto.TargetSessionID, req proto.Request) (restore func()) {
81 | _, enabled := b.states.Load(b.key(sessionID, req.ProtoReq()))
82 | domain, _ := proto.ParseMethodName(req.ProtoReq())
83 |
84 | if enabled {
85 | _, _ = b.Call(b.ctx, string(sessionID), domain+".disable", nil)
86 | }
87 |
88 | return func() {
89 | if enabled {
90 | _, _ = b.Call(b.ctx, string(sessionID), req.ProtoReq(), req)
91 | }
92 | }
93 | }
94 |
95 | func (b *Browser) cachePage(page *Page) {
96 | b.states.Store(page.TargetID, page)
97 | }
98 |
99 | func (b *Browser) loadCachedPage(id proto.TargetTargetID) *Page {
100 | if cache, ok := b.states.Load(id); ok {
101 | return cache.(*Page)
102 | }
103 | return nil
104 | }
105 |
106 | // LoadState into the method.
107 | // 在方法中的LoadState
108 | func (p *Page) LoadState(method proto.Request) (has bool) {
109 | return p.browser.LoadState(p.SessionID, method)
110 | }
111 |
112 | // EnableDomain and returns a restore function to restore previous state
113 | // EnableDomain 返回一个恢复函数来恢复之前的 State
114 | func (p *Page) EnableDomain(method proto.Request) (restore func()) {
115 | return p.browser.Context(p.ctx).EnableDomain(p.SessionID, method)
116 | }
117 |
118 | // DisableDomain and returns a restore function to restore previous state
119 | // DisableDomain 返回一个恢复函数来恢复之前的状态。
120 | func (p *Page) DisableDomain(method proto.Request) (restore func()) {
121 | return p.browser.Context(p.ctx).DisableDomain(p.SessionID, method)
122 | }
123 |
124 | func (p *Page) cleanupStates() {
125 | p.browser.RemoveState(p.TargetID)
126 | }
127 |
--------------------------------------------------------------------------------