├── .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 | [![Go Reference](https://pkg.go.dev/badge/github.com/go-rod/go-rod-chinese.svg)](https://pkg.go.dev/github.com/go-rod/go-rod-chinese) 4 | [![Discord Chat](https://img.shields.io/discord/719933559456006165.svg)][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 | 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 | 51 | %s 52 |
`, 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 |
7 | 11 | 12 |
13 | 14 | 18 | 19 |
20 | 21 | 25 | 26 |
27 | 28 | 34 | 35 |
36 | 37 | 42 | 43 |
44 | 45 |
46 | 47 |
48 | 49 | 50 | 51 |
52 | 53 | 57 | 58 |
59 | 60 | 66 | 67 |
68 | 69 | 70 |
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 | 3 | 4 | mouse-pointer 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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 | img 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 | 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 | 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 |
51 |

box3

52 |

53 | box4 text 54 |

55 |

56 | 57 | 63 |

64 |
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 |
71 | 72 | 73 |
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 |
54 |

box3

55 |

56 | box4 text 57 |

58 |

59 | 60 | 66 |

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